From d67223de9b9e981cebecbd4a788e117ef418b0f6 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:16:38 +0200 Subject: [PATCH 1/5] chore: use fully qualified tmux pane targets in webmux systemPrompt (#8651) * fix: use fully qualified tmux pane targets in webmux systemPrompt Co-Authored-By: Claude Opus 4.6 (1M context) * fix: anchor tmux pane targets to $TMUX_PANE for stability across window switches Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .webmux.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.webmux.yaml b/.webmux.yaml index c41d0aa699..e00435465d 100644 --- a/.webmux.yaml +++ b/.webmux.yaml @@ -43,7 +43,7 @@ profiles: - Pane 0: this pane (claude agent) - Pane 1: backend (cargo watch -x run) - Pane 2: frontend (npm run dev) - To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend). + To check logs, use: \`tmux capture-pane -t $(tmux display-message -t "$TMUX_PANE" -p '#{session_name}:#{window_name}').1 -p -S -50\` (backend) or \`tmux capture-pane -t $(tmux display-message -t "$TMUX_PANE" -p '#{session_name}:#{window_name}').2 -p -S -50\` (frontend). For this window specifically, backend is running on: ${BACKEND_PORT} and frontend is running on: ${FRONTEND_PORT}. To connect to the database, use this connection string: ${DATABASE_URL} Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check. @@ -72,7 +72,7 @@ profiles: Pane layout (current window): - Pane 0: this pane (claude agent) - Pane 1: frontend (npm run dev) - To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (frontend). + To check logs, use: \`tmux capture-pane -t $(tmux display-message -t "$TMUX_PANE" -p '#{session_name}:#{window_name}').1 -p -S -50\` (frontend). On this window specifically, frontend is running on: ${FRONTEND_PORT}. To connect to the database, use this connection string: ${DATABASE_URL} Because we are running frontend with npm run dev, to verify your changes, just check the logs in the frontend pane. No need for npm run build. From 2862c1cf566906855bcd5c87a2b38238bbb6a196 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:21:39 +0200 Subject: [PATCH 2/5] add codex PR review workflow (#8626) * feat: add codex PR review workflow * refactor: simplify codex PR review comments * chore: use ubicloud for codex review * fix: harden codex review workflow * chore: use chatgpt auth for codex review --- .github/codex/pr-review.prompt.md | 23 ++++ .github/workflows/codex-pr-review.yml | 145 ++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 .github/codex/pr-review.prompt.md create mode 100644 .github/workflows/codex-pr-review.yml diff --git a/.github/codex/pr-review.prompt.md b/.github/codex/pr-review.prompt.md new file mode 100644 index 0000000000..d3e6dfc4e8 --- /dev/null +++ b/.github/codex/pr-review.prompt.md @@ -0,0 +1,23 @@ +You are reviewing a GitHub pull request for this repository. + +Review policy: +- Read `CLAUDE.md` before reviewing code. +- Only report issues you are confident are real and introduced by this pull request. +- Focus on bugs, security problems, and clear `CLAUDE.md` violations. +- Do not report style nits, speculative concerns, pre-existing issues, or problems that a normal linter/typechecker would obviously catch. +- Keep the review high signal. If there is no clear issue, return no findings. + +Repository context: +- Read `./.github/codex/pr-review-context.md` for the PR metadata and the exact diff commands to use. +- Review only the changes introduced by this PR. +- Read additional files only when the diff is not enough to validate a finding. +- Do not modify any files. + +Output requirements: +- Return a GitHub PR comment in markdown, not JSON. +- Start with `## Codex Review`. +- Give a short overall summary first. +- If you found high-signal issues, list them in a short numbered list with file paths and line numbers when you know them confidently. +- If you found no high-signal issues, say that explicitly. +- End with a `### Reproduction instructions` section containing a short descriptive paragraph for a tester explaining how to navigate the app to observe the change. Do not make it a numbered list. If the diff is not enough to infer this safely, say that plainly. +- Prefer at most 10 findings. diff --git a/.github/workflows/codex-pr-review.yml b/.github/workflows/codex-pr-review.yml new file mode 100644 index 0000000000..e945f5fd45 --- /dev/null +++ b/.github/workflows/codex-pr-review.yml @@ -0,0 +1,145 @@ +name: Codex Auto Review + +on: + pull_request: + types: [ready_for_review, opened] + +concurrency: + group: codex-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + codex-review: + runs-on: ubicloud-standard-2 + timeout-minutes: 30 + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false + permissions: + contents: read + issues: write + steps: + - name: Check Codex configuration + id: codex_config + env: + CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} + run: | + if [ -n "$CODEX_AUTH_JSON" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "CODEX_AUTH_JSON is not configured; skipping Codex review." + fi + + - name: Checkout repository + if: steps.codex_config.outputs.enabled == 'true' + uses: actions/checkout@v5 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/merge + fetch-depth: 1 + + - name: Set up Node.js + if: steps.codex_config.outputs.enabled == 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Codex CLI + if: steps.codex_config.outputs.enabled == 'true' + run: npm install --global @openai/codex@0.117.0 + + - name: Configure file-backed Codex auth + if: steps.codex_config.outputs.enabled == 'true' + env: + CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} + run: | + CODEX_HOME="$HOME/.codex" + echo "CODEX_HOME=$CODEX_HOME" >> "$GITHUB_ENV" + mkdir -p "$CODEX_HOME" + chmod 700 "$CODEX_HOME" + cat > "$CODEX_HOME/config.toml" <<'EOF' + cli_auth_credentials_store = "file" + EOF + printf '%s' "$CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json" + chmod 600 "$CODEX_HOME/auth.json" + node -e 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))' "$CODEX_HOME/auth.json" + + - name: Pre-fetch base and head refs for the PR + if: steps.codex_config.outputs.enabled == 'true' + env: + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git fetch --no-tags origin \ + "$PR_BASE_REF" \ + "+refs/pull/$PR_NUMBER/head" + + - name: Write Codex review context + if: steps.codex_config.outputs.enabled == 'true' + env: + PR_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body || '' }} + run: | + mkdir -p .github/codex + node <<'NODE' + const fs = require('fs'); + const lines = [ + `Repository: ${process.env.PR_REPOSITORY}`, + `PR number: ${process.env.PR_NUMBER}`, + `Base SHA: ${process.env.PR_BASE_SHA}`, + `Head SHA: ${process.env.PR_HEAD_SHA}`, + '', + 'PR title:', + process.env.PR_TITLE || '(empty)', + '', + 'PR body:', + process.env.PR_BODY || '(empty)', + '', + 'Changed commits command:', + `git log --oneline ${process.env.PR_BASE_SHA}...${process.env.PR_HEAD_SHA}`, + '', + 'Changed files command:', + `git diff --stat ${process.env.PR_BASE_SHA}...${process.env.PR_HEAD_SHA}`, + '', + 'Full review diff command:', + `git diff --unified=0 ${process.env.PR_BASE_SHA}...${process.env.PR_HEAD_SHA}` + ]; + fs.writeFileSync('.github/codex/pr-review-context.md', `${lines.join('\n')}\n`); + NODE + + - name: Run Codex review + if: steps.codex_config.outputs.enabled == 'true' + run: | + codex exec \ + -C "$GITHUB_WORKSPACE" \ + -m gpt-5.4 \ + -c 'model_reasoning_effort="xhigh"' \ + -s read-only \ + -o codex-final-message.md \ + - < .github/codex/pr-review.prompt.md + + - name: Post Codex review comment + if: steps.codex_config.outputs.enabled == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ github.token }} + script: | + const fs = require('fs'); + const path = `${process.env.GITHUB_WORKSPACE}/codex-final-message.md`; + if (!fs.existsSync(path)) { + core.info('Codex did not produce a final message; skipping PR comment.'); + return; + } + const body = fs.readFileSync(path, 'utf8').trim(); + if (!body) { + core.info('Codex final message was empty; skipping PR comment.'); + return; + } + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body, + }); From df7a8eebcf34287749e5668db1c697293cf36e42 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 31 Mar 2026 21:26:34 +0000 Subject: [PATCH 3/5] chore(main): release 1.671.0 (#8650) * chore(main): release 1.671.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- CHANGELOG.md | 14 + backend/Cargo.lock | 245 +++++++++--------- backend/Cargo.toml | 4 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/main.ts | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- lsp/Pipfile | 2 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- 15 files changed, 151 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e8710b1be..9a2d3e0ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.671.0](https://github.com/windmill-labs/windmill/compare/v1.670.0...v1.671.0) (2026-03-31) + + +### Features + +* add configurable preview job tag override in default tags settings ([#8649](https://github.com/windmill-labs/windmill/issues/8649)) ([da8886b](https://github.com/windmill-labs/windmill/commit/da8886be8575dd925b6d24c55ab379bc6984c5f8)) +* improve CLI flow log streaming and job inspection ([#8644](https://github.com/windmill-labs/windmill/issues/8644)) ([6c3c971](https://github.com/windmill-labs/windmill/commit/6c3c971af5aa1362632ee0deeddf91b8bc47c853)) +* support hub flows in raw app runnables ([#8627](https://github.com/windmill-labs/windmill/issues/8627)) ([040a199](https://github.com/windmill-labs/windmill/commit/040a199685cea5c99c944bacb5584a381d6ec829)) + + +### Bug Fixes + +* return default_args/enums in approval info and fix subflow resume buttons ([#8648](https://github.com/windmill-labs/windmill/issues/8648)) ([852c59e](https://github.com/windmill-labs/windmill/commit/852c59efbb04510e5e6f99919707effcf6769a2f)) + ## [1.670.0](https://github.com/windmill-labs/windmill/compare/v1.669.1...v1.670.0) (2026-03-31) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 8e7df014a2..20e0d8b31a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1202,7 +1202,7 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", @@ -1396,7 +1396,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "itoa", "matchit 0.8.4", @@ -1723,16 +1723,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq 0.4.2", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] @@ -1808,7 +1808,7 @@ dependencies = [ "hex", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -3981,7 +3981,7 @@ dependencies = [ "hickory-resolver", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.27.7", "hyper-util", "ipnet", @@ -4069,7 +4069,7 @@ dependencies = [ "http 1.4.0", "httparse", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "itertools 0.10.5", "memmem", @@ -4262,7 +4262,7 @@ dependencies = [ "hkdf", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "idna", "indexmap 2.12.0", @@ -4533,7 +4533,7 @@ dependencies = [ "http 1.4.0", "http-body-util", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "libc", "log", @@ -4587,7 +4587,7 @@ dependencies = [ "deno_error", "deno_tls", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.27.7", "hyper-util", "log", @@ -4717,7 +4717,7 @@ dependencies = [ "h2 0.4.13", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "once_cell", "rustls-tokio-stream", @@ -5633,7 +5633,7 @@ dependencies = [ "base64 0.21.7", "bytes", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project", "rand 0.8.5", @@ -6943,7 +6943,7 @@ dependencies = [ "futures", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.26.0", "hyper-tls", "hyper-tungstenite", @@ -6969,9 +6969,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ "typenum", ] @@ -7002,9 +7002,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -7017,7 +7017,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -7033,7 +7032,7 @@ dependencies = [ "futures-util", "headers", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", @@ -7053,7 +7052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7085,7 +7084,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "log", "rustls 0.22.4", @@ -7103,7 +7102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "log", "rustls 0.23.35", @@ -7121,7 +7120,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7136,7 +7135,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "native-tls", "tokio", @@ -7151,7 +7150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a343d17fe7885302ed7252767dc7bb83609a874b6ff581142241ec4b73957ad" dependencies = [ "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7171,7 +7170,7 @@ dependencies = [ "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", @@ -7192,7 +7191,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7463,9 +7462,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8062b737e5389949f477d4760a2ebbff0c366f97798f2419b8d8f366363d3342" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] @@ -7856,7 +7855,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-http-proxy", "hyper-rustls 0.27.7", "hyper-timeout", @@ -9416,7 +9415,7 @@ dependencies = [ "http-body-util", "httparse", "humantime", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "md-5 0.10.6", "parking_lot", @@ -11257,7 +11256,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", @@ -11305,7 +11304,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.27.7", "hyper-util", "js-sys", @@ -11361,7 +11360,7 @@ dependencies = [ "futures", "getrandom 0.2.17", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "reqwest 0.13.1", "reqwest-middleware", "retry-policies", @@ -14649,7 +14648,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -14681,7 +14680,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -15932,7 +15931,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.670.0" +version = "1.671.0" dependencies = [ "anyhow", "async-nats", @@ -16010,7 +16009,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16023,7 +16022,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "argon2", @@ -16058,7 +16057,7 @@ dependencies = [ "hex", "hmac 0.12.1", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "indexmap 2.12.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -16164,12 +16163,12 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "quick_cache", "serde", @@ -16187,7 +16186,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16200,7 +16199,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16226,7 +16225,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.669.1" +version = "1.671.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -16236,7 +16235,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16253,7 +16252,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "base64 0.22.1", @@ -16276,7 +16275,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16299,7 +16298,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16315,11 +16314,11 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", - "hyper 1.8.1", + "hyper 1.9.0", "serde", "serde_json", "sql-builder", @@ -16335,7 +16334,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16355,7 +16354,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16369,7 +16368,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-nats", @@ -16400,14 +16399,14 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "axum 0.8.4", "base64 0.22.1", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "serde", "serde_json", @@ -16425,7 +16424,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "flate2", @@ -16443,7 +16442,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16465,7 +16464,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16485,13 +16484,13 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", "futures", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "lazy_static", "quick_cache", @@ -16515,7 +16514,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16542,7 +16541,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.669.1" +version = "1.671.0" dependencies = [ "lazy_static", "serde", @@ -16554,13 +16553,13 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.669.1" +version = "1.671.0" dependencies = [ "argon2", "axum 0.8.4", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "serde", "serde_json", @@ -16578,7 +16577,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16592,13 +16591,13 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.669.1" +version = "1.671.0" dependencies = [ "axum 0.8.4", "chrono", "hex", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "magic-crypt", "regex", @@ -16624,7 +16623,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.669.1" +version = "1.671.0" dependencies = [ "chrono", "lazy_static", @@ -16638,7 +16637,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16657,7 +16656,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.669.1" +version = "1.671.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -16694,7 +16693,7 @@ dependencies = [ "globset", "hex", "hmac 0.12.1", - "hyper 1.8.1", + "hyper 1.9.0", "indexmap 2.12.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -16759,7 +16758,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.669.1" +version = "1.671.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -16778,7 +16777,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.669.1" +version = "1.671.0" dependencies = [ "regex", "serde", @@ -16793,7 +16792,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -16817,7 +16816,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "futures", @@ -16834,7 +16833,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.669.1" +version = "1.671.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -16850,7 +16849,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -16871,7 +16870,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -16902,7 +16901,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-oauth2", @@ -16926,7 +16925,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-stream", @@ -16960,7 +16959,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "futures", @@ -16978,7 +16977,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.669.1" +version = "1.671.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -16987,7 +16986,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "lazy_static", @@ -16999,7 +16998,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "serde_json", @@ -17011,7 +17010,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "gosyn", @@ -17023,7 +17022,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "lazy_static", @@ -17035,7 +17034,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "serde_json", @@ -17047,7 +17046,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "nu-parser", @@ -17058,7 +17057,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -17069,7 +17068,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -17081,7 +17080,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "rustpython-ast", @@ -17092,7 +17091,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-recursion", @@ -17114,7 +17113,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "lazy_static", @@ -17128,7 +17127,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -17145,7 +17144,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "lazy_static", @@ -17158,7 +17157,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "serde", @@ -17170,7 +17169,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "lazy_static", @@ -17188,7 +17187,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -17204,7 +17203,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "rustpython-ast", @@ -17220,7 +17219,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "serde", @@ -17231,7 +17230,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-recursion", @@ -17268,7 +17267,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "const_format", @@ -17306,7 +17305,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.669.1" +version = "1.671.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -17317,7 +17316,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-recursion", @@ -17325,7 +17324,7 @@ dependencies = [ "chrono", "futures", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "quick_cache", "reqwest 0.13.1", @@ -17346,7 +17345,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17370,14 +17369,14 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", "axum 0.8.4", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "lazy_static", "rand 0.9.0", @@ -17403,7 +17402,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17423,7 +17422,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17457,7 +17456,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17469,7 +17468,7 @@ dependencies = [ "hex", "hmac 0.12.1", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "lazy_static", "matchit 0.7.3", @@ -17493,7 +17492,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17516,7 +17515,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17540,7 +17539,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-nats", @@ -17564,7 +17563,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17599,7 +17598,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17627,7 +17626,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-trait", @@ -17650,7 +17649,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "bitflags 2.9.4", @@ -17669,7 +17668,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.669.1" +version = "1.671.0" dependencies = [ "anyhow", "async-once-cell", @@ -17778,7 +17777,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.669.1" +version = "1.671.0" dependencies = [ "bytes", "futures", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4c244e2cf8..0e010f0b3f 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.670.0" +version = "1.671.0" authors.workspace = true edition.workspace = true @@ -82,7 +82,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.670.0" +version = "1.671.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index ce8932edc4..78624fd5f9 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.670.0 + version: 1.671.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 36f6bebac6..350c26020c 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -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.670.0"; +export const VERSION = "v1.671.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index a6048b5a84..c3f1812ad8 100755 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -78,7 +78,7 @@ export { token, }; -export const VERSION = "1.670.0"; +export const VERSION = "1.671.0"; // Re-exported from constants.ts to maintain backwards compatibility export { WM_FORK_PREFIX } from "./core/constants.ts"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 650ea5bc1c..5a6ba807ca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.670.0", + "version": "1.671.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.670.0", + "version": "1.671.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 4d4379849a..c876f4f26e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.670.0", + "version": "1.671.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/lsp/Pipfile b/lsp/Pipfile index 99d6d99817..2fd8f8f2d2 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.670.0" +wmill = ">=1.671.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 15da1b2b87..8889703621 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.670.0 + version: 1.671.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index a459716bda..d9dc741dc2 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.670.0' + ModuleVersion = '1.671.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 9f913e73d6..332abcadde 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.670.0" +version = "1.671.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index eb05f7381c..c1bc361c50 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.670.0", + "version": "1.671.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index b8831c8ca7..988b14d95c 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.670.0", + "version": "1.671.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "sideEffects": false, diff --git a/version.txt b/version.txt index bab330cdba..fe78d20226 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.670.0 +1.671.0 From 70692021909443b86ed61fa621fe49f28742fb54 Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Tue, 31 Mar 2026 23:25:22 -0400 Subject: [PATCH 4/5] fix: approval page freeze, stale state, and missing approval link (#8653) * fix: prevent browser freeze when approval form number field has no default value Co-Authored-By: Claude Opus 4.5 * fix: disable approval buttons and keep polling after approve/deny action Co-Authored-By: Claude Opus 4.5 * fix: restore approval page link and prevent double resume in flow viewer Co-Authored-By: Claude Opus 4.5 * fix: guard against NaN fallback in Range and reset actionTaken on new approval step Co-Authored-By: Claude Opus 4.6 (1M context) * fix approval page url --------- Co-authored-by: Claude Opus 4.5 --- .../FlowStatusWaitingForEvents.svelte | 29 +++++++++++++--- frontend/src/lib/components/Range.svelte | 12 +++++-- .../approve/[workspace]/[job]/+page.svelte | 33 +++++++++---------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/frontend/src/lib/components/FlowStatusWaitingForEvents.svelte b/frontend/src/lib/components/FlowStatusWaitingForEvents.svelte index 195a1d6fad..1b219153da 100644 --- a/frontend/src/lib/components/FlowStatusWaitingForEvents.svelte +++ b/frontend/src/lib/components/FlowStatusWaitingForEvents.svelte @@ -3,7 +3,7 @@ import { type Job, JobService } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { sendUserToast } from '$lib/toast' - import { X } from 'lucide-svelte' + import { ExternalLink, X } from 'lucide-svelte' import DisplayResult from './DisplayResult.svelte' import Tooltip from './Tooltip.svelte' import { Button } from './common' @@ -23,6 +23,7 @@ let default_payload: object = $state({}) let description: any = $state(undefined) let hide_cancel = $state(false) + let approvalPageUrl: string | undefined = $state(undefined) let defaultValues = $state({}) @@ -47,6 +48,8 @@ defaultValues = JSON.parse(JSON.stringify(args)) default_payload = args + approvalPageUrl = job_result?.['approvalPage'] + actionTaken = false hide_cancel = job?.raw_flow?.modules?.[approvalStep]?.suspend?.hide_cancel ?? false schema = mergeSchema( job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema ?? {}, @@ -55,6 +58,7 @@ } let loading = $state(false) + let actionTaken = $state(false) async function continu(approve: boolean) { loading = true try { @@ -66,6 +70,7 @@ approved: approve } }) + actionTaken = true } catch (e: any) { sendUserToast(e?.body ?? e?.message ?? 'Failed', true) } finally { @@ -84,7 +89,7 @@
{/if}
-
+
{#if !hide_cancel}
{/if}
-
+ {#if approvalPageUrl} + + Approval page + + {/if} + {#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
`${v}`, @@ -36,8 +36,14 @@ } run(() => { - if (value === null) { - value = 0 + if (value === null || value === undefined || Number.isNaN(value)) { + const fallback = + initialValue !== undefined + ? typeof initialValue === 'string' + ? parseInt(initialValue) + : initialValue + : (min ?? 0) + value = Number.isNaN(fallback) ? (min ?? 0) : fallback } }) diff --git a/frontend/src/routes/approve/[workspace]/[job]/+page.svelte b/frontend/src/routes/approve/[workspace]/[job]/+page.svelte index ff664a0297..4055b4e62b 100644 --- a/frontend/src/routes/approve/[workspace]/[job]/+page.svelte +++ b/frontend/src/routes/approve/[workspace]/[job]/+page.svelte @@ -33,6 +33,7 @@ let default_payload: any = $state({}) let loading = $state(false) let valid = $state(true) + let actionTaken: 'approved' | 'denied' | undefined = $state(undefined) let pollInterval: number | undefined = undefined let scheduleEditor: ScheduleEditor | undefined = $state(undefined) @@ -81,6 +82,9 @@ id: page.params.job ?? '' })) as Job completed = job?.type === 'CompletedJob' + if (completed) { + pollInterval && clearInterval(pollInterval) + } } catch { // Job details are optional — page works with just approvalInfo } @@ -103,7 +107,7 @@ } }) sendUserToast('Flow approved') - pollInterval && clearInterval(pollInterval) + actionTaken = 'approved' loadData() } catch (e: any) { sendUserToast(e?.body ?? e?.message ?? 'Failed to approve', true) @@ -125,7 +129,7 @@ } }) sendUserToast('Flow denied!') - pollInterval && clearInterval(pollInterval) + actionTaken = 'denied' loadData() } catch (e: any) { sendUserToast(e?.body ?? e?.message ?? 'Failed to cancel', true) @@ -259,6 +263,12 @@ The flow is not running anymore. You cannot cancel or resume it. + {:else if actionTaken} + + {actionTaken === 'approved' + ? 'You have approved this flow. Waiting for it to complete...' + : 'You have denied this flow. Waiting for it to complete...'} + {/if} {#if approvalInfo.description != undefined} @@ -279,31 +289,20 @@ {/if} {/if} - {#if !completed && approvalInfo.can_approve} + {#if !completed && !actionTaken && approvalInfo.can_approve}
{#if approvalInfo.hide_cancel !== true} - {:else}
{/if} -
- {:else if !completed && !approvalInfo.can_approve} + {:else if !completed && !actionTaken && !approvalInfo.can_approve} {#if approvalInfo.user_auth_required && !$userStore} {:else} From a46aa641f9d72809c52a0eb11a877a0f2d587c32 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 1 Apr 2026 06:11:37 +0000 Subject: [PATCH 5/5] feat: add R language support (#8263) * feat: add R language support Add R as a new supported scripting language in Windmill, following the same pattern used for Ruby. Includes: - Backend: ScriptLang::Rlang enum variant, DB migration, tree-sitter-r parser crate with tests, WASM parser binding, R executor with NSJail sandboxing, job dispatch and signature parsing - Frontend: language picker, R icon, syntax highlighting, editor bar insertions (Sys.getenv, get_variable, get_resource), schema inference, init code template, BETA badge - CLI: .r extension mapping, sync support, bootstrap template R scripts use `main <- function(...)` syntax, jsonlite for JSON serialization, and system curl for the Windmill client helper. Co-Authored-By: Claude Opus 4.6 * feat: add R package resolution and installation Parse library()/require() calls from R scripts to extract dependencies. Resolve versions from CRAN, cache lockfiles in pip_resolution_cache, and install packages to a shared R library cache. The run step sets R_LIBS_USER so installed packages are available to the script. - Parser: parse_r_requirements() extracts package names from AST - Executor: resolve() generates lockfile, install() installs from CRAN - Worker lockfiles: wire up R resolve for dependency jobs Co-Authored-By: Claude Opus 4.6 * feat: add nsjail sandboxing for R resolve and install phases Co-Authored-By: Claude Opus 4.6 * fix: fix R get_variable/get_resource and add sandbox annotation + e2e tests Co-Authored-By: Claude Opus 4.6 * fix: fix R arg inference with JS fallback parser and get_variable/get_resource Co-Authored-By: Claude Opus 4.6 * fix flake * nsjail * nits * fix: R install improvements - suppress verbose output, flat lockfile logging, Dockerfile R support, rlimits - Suppress renv verbose output during resolve and install (controlled by #verbose annotation) - Filter renv from install list (already loaded, causes noisy restart message) - Log compact "resolved N packages" instead of full renv.lock JSON - Add R (r-base, r-cran-renv) to DockerfileFull and DockerfileFullEe - Use disable_rl for nsjail install config (R compiles from source) - Reduce default concurrency from 20 to 5 - Add rlang to openflow.openapi.yaml - Fix MainArgSignature (no_main_func -> auto_kind) after main merge Co-Authored-By: Claude Opus 4.6 * final * fix: remove accidental R install from multiplayer Dockerfile Co-Authored-By: Claude Opus 4.5 * fix: remove R from Windows build and DockerfileExtra Co-Authored-By: Claude Opus 4.5 * fix: rename R migration to avoid timestamp collision with trigger_filter_logic Co-Authored-By: Claude Opus 4.5 * all * fix: R install improvements - suppress verbose output, flat lockfile logging, Dockerfile R support, rlimits Co-Authored-By: Claude Opus 4.5 * fix: add clear error when Rscript binary is missing Co-Authored-By: Claude Opus 4.5 * fix: fix type errors in R fallback parser, use format! in wrap(), add R system prompts Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: pyranota --- backend/Cargo.lock | 27 +- backend/Cargo.toml | 8 +- .../20260331000000_add_rlang.down.sql | 0 .../20260331000000_add_rlang.up.sql | 2 + backend/parsers/windmill-parser-r/Cargo.toml | 17 + backend/parsers/windmill-parser-r/src/lib.rs | 363 +++++++++ .../windmill-parser-r/src/wasm_libc.rs | 293 +++++++ .../parsers/windmill-parser-wasm/Cargo.toml | 2 + backend/parsers/windmill-parser-wasm/build.nu | 5 + .../parsers/windmill-parser-wasm/src/lib.rs | 6 + .../wasm-sysroot/string.h | 2 + backend/src/main.rs | 3 +- backend/summarized_schema.txt | 2 +- backend/tests/worker.rs | 109 +++ backend/windmill-api-scripts/src/scripts.rs | 1 + backend/windmill-api/openapi.yaml | 1 + backend/windmill-api/src/workspaces_export.rs | 1 + .../windmill-common/src/global_settings.rs | 1 + .../windmill-common/src/instance_config.rs | 1 + backend/windmill-common/src/worker.rs | 8 + .../src/windmill-client.js | 1 + backend/windmill-types/src/scripts.rs | 5 +- backend/windmill-worker/Cargo.toml | 2 + .../nsjail/install.r.config.proto | 100 +++ .../windmill-worker/nsjail/run.r.config.proto | 125 +++ backend/windmill-worker/src/lib.rs | 3 + backend/windmill-worker/src/r_executor.rs | 715 ++++++++++++++++++ backend/windmill-worker/src/ruby_executor.rs | 6 +- .../src/universal_pkg_installer.rs | 367 +++++++-- backend/windmill-worker/src/worker.rs | 45 +- .../windmill-worker/src/worker_lockfiles.rs | 17 + cli/bootstrap/script_bootstrap.ts | 5 + cli/src/commands/script/script.ts | 3 + cli/src/commands/sync/sync.ts | 1 + cli/src/guidance/skills.ts | 104 ++- cli/src/types.ts | 1 + cli/src/utils/metadata.ts | 3 + cli/src/utils/script_common.ts | 3 + .../src/path-utils/path-assigner.ts | 1 + docker/DockerfileFull | 4 + docker/DockerfileFullEe | 4 + flake.nix | 177 +++-- frontend/package-lock.json | 56 +- frontend/package.json | 1 + frontend/src/lib/components/EditorBar.svelte | 13 +- .../src/lib/components/HighlightCode.svelte | 3 + .../src/lib/components/ScriptBuilder.svelte | 2 +- .../common/languageIcons/LanguageIcon.svelte | 3 + .../src/lib/components/icons/RIcon.svelte | 37 + frontend/src/lib/components/worker_group.ts | 1 + frontend/src/lib/editorLangUtils.ts | 2 + frontend/src/lib/infer.ts | 86 +++ frontend/src/lib/script_helpers.ts | 25 + frontend/src/lib/scripts.ts | 7 +- openflow.openapi.yaml | 1 + system_prompts/auto-generated/flow.md | 2 +- system_prompts/auto-generated/prompts.ts | 89 ++- system_prompts/auto-generated/script.md | 87 +++ .../auto-generated/skills/write-flow/SKILL.md | 2 +- .../skills/write-script-rlang/SKILL.md | 100 +++ system_prompts/languages/rlang.md | 85 +++ system_prompts/utils.py | 5 + 62 files changed, 2977 insertions(+), 174 deletions(-) create mode 100644 backend/migrations/20260331000000_add_rlang.down.sql create mode 100644 backend/migrations/20260331000000_add_rlang.up.sql create mode 100644 backend/parsers/windmill-parser-r/Cargo.toml create mode 100644 backend/parsers/windmill-parser-r/src/lib.rs create mode 100644 backend/parsers/windmill-parser-r/src/wasm_libc.rs create mode 100644 backend/windmill-worker/nsjail/install.r.config.proto create mode 100644 backend/windmill-worker/nsjail/run.r.config.proto create mode 100644 backend/windmill-worker/src/r_executor.rs create mode 100644 frontend/src/lib/components/icons/RIcon.svelte create mode 100644 system_prompts/auto-generated/skills/write-script-rlang/SKILL.md create mode 100644 system_prompts/languages/rlang.md diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 20e0d8b31a..1114ae5f8e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -14625,9 +14625,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" dependencies = [ "winnow 1.0.1", ] @@ -14943,6 +14943,16 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" +[[package]] +name = "tree-sitter-r" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "429133cbda9f8a46e03ef3aae6abb6c3d22875f8585cad472138101bfd517255" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-ruby" version = "0.23.1" @@ -17111,6 +17121,18 @@ dependencies = [ "windmill-parser", ] +[[package]] +name = "windmill-parser-r" +version = "1.671.0" +dependencies = [ + "anyhow", + "serde_json", + "tree-sitter", + "tree-sitter-r", + "wasm-bindgen", + "windmill-parser", +] + [[package]] name = "windmill-parser-ruby" version = "1.671.0" @@ -17762,6 +17784,7 @@ dependencies = [ "windmill-parser-php", "windmill-parser-py", "windmill-parser-py-imports", + "windmill-parser-r", "windmill-parser-ruby", "windmill-parser-rust", "windmill-parser-sql", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 0e010f0b3f..cdfd8032d0 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -66,10 +66,13 @@ members = [ "./parsers/windmill-parser-nu", "./parsers/windmill-parser-java", "./parsers/windmill-parser-ruby", + "./parsers/windmill-parser-r", "./parsers/windmill-parser-bash", "./parsers/windmill-parser-py", "./parsers/windmill-parser-py-asset", "./parsers/windmill-parser-py-imports", + # Uncomment to build wasm parsers: + # "./parsers/windmill-parser-wasm", "./parsers/windmill-parser-wac", "./parsers/windmill-parser-sql", "./parsers/windmill-parser-sql-asset", @@ -163,7 +166,8 @@ csharp = ["windmill-worker/csharp"] nu = ["windmill-worker/nu"] java = ["windmill-worker/java"] ruby = ["windmill-worker/ruby"] -all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql-kerberos", "bigquery", "csharp", "nu", "php", "java", "ruby"] +rlang = ["windmill-worker/rlang"] +all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql-kerberos", "bigquery", "csharp", "nu", "php", "java", "ruby", "rlang"] # For windows we have another set of languages enabled all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql-winauth", "bigquery", "csharp", "nu", "php", "java"] # Edition meta-features: shared groups @@ -347,6 +351,7 @@ windmill-parser-yaml = { path = "./parsers/windmill-parser-yaml" } windmill-parser-csharp = { path = "./parsers/windmill-parser-csharp" } windmill-parser-java = { path = "./parsers/windmill-parser-java" } windmill-parser-ruby = { path = "./parsers/windmill-parser-ruby" } +windmill-parser-r = { path = "./parsers/windmill-parser-r" } windmill-parser-nu = { path = "./parsers/windmill-parser-nu" } windmill-parser-bash = { path = "./parsers/windmill-parser-bash" } windmill-parser-sql = { path = "./parsers/windmill-parser-sql" } @@ -613,6 +618,7 @@ tree-sitter = { version = "0.23.0", features = [] } tree-sitter-c-sharp = "0.23.0" tree-sitter-java = "0.23.0" tree-sitter-ruby = "0.23.0" +tree-sitter-r = "1.2.0" oracle = { version = "0.6.3", features = ["chrono"] } rumqttc = { version = "0.24.0", features = ["use-native-tls"]} strum = { version = "0.27", features = ["derive"] } diff --git a/backend/migrations/20260331000000_add_rlang.down.sql b/backend/migrations/20260331000000_add_rlang.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/migrations/20260331000000_add_rlang.up.sql b/backend/migrations/20260331000000_add_rlang.up.sql new file mode 100644 index 0000000000..cfcb852946 --- /dev/null +++ b/backend/migrations/20260331000000_add_rlang.up.sql @@ -0,0 +1,2 @@ +ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'rlang'; +UPDATE config SET config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["rlang"]'::jsonb) WHERE name = 'worker__default' AND config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php", "rust", "ansible", "csharp", "nu", "java", "duckdb", "ruby"]}'::jsonb AND NOT config->'worker_tags' @> '"rlang"'::jsonb; diff --git a/backend/parsers/windmill-parser-r/Cargo.toml b/backend/parsers/windmill-parser-r/Cargo.toml new file mode 100644 index 0000000000..42701f9c12 --- /dev/null +++ b/backend/parsers/windmill-parser-r/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "windmill-parser-r" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[lib] +name = "windmill_parser_r" +path = "./src/lib.rs" + +[dependencies] +windmill-parser.workspace = true +tree-sitter.workspace = true +tree-sitter-r.workspace = true +anyhow.workspace = true +wasm-bindgen.workspace = true +serde_json.workspace = true diff --git a/backend/parsers/windmill-parser-r/src/lib.rs b/backend/parsers/windmill-parser-r/src/lib.rs new file mode 100644 index 0000000000..4d018fa8cc --- /dev/null +++ b/backend/parsers/windmill-parser-r/src/lib.rs @@ -0,0 +1,363 @@ +#![cfg_attr(target_arch = "wasm32", feature(c_variadic))] + +#[cfg(target_arch = "wasm32")] +pub mod wasm_libc; + +use anyhow::anyhow; +use serde_json::Value; +use tree_sitter::Node; +use tree_sitter::Range; +use windmill_parser::json_to_typ; +use windmill_parser::Arg; +use windmill_parser::MainArgSignature; + +pub fn parse_r_sig_meta(code: &str) -> anyhow::Result { + let mut parser = tree_sitter::Parser::new(); + let language = tree_sitter_r::LANGUAGE; + parser + .set_language(&language.into()) + .map_err(|e| anyhow!("Error setting R as language: {e}"))?; + + let tree = parser + .parse(code, None) + .ok_or(anyhow!("Failed to parse code"))?; + let root_node = tree.root_node(); + + let args = find_main_signature(root_node, code)?; + let main_sig = MainArgSignature { + star_args: false, + star_kwargs: false, + args: args.unwrap_or_default(), + has_preprocessor: None, + auto_kind: None, + }; + + Ok(main_sig) +} + +pub fn parse_r_signature(code: &str) -> anyhow::Result { + Ok(parse_r_sig_meta(code)?) +} + +/// Extract package names from `library(...)` and `require(...)` calls in R code. +/// Returns a newline-separated list of package names. +pub fn parse_r_requirements(code: &str) -> anyhow::Result { + let mut parser = tree_sitter::Parser::new(); + let language = tree_sitter_r::LANGUAGE; + parser + .set_language(&language.into()) + .map_err(|e| anyhow!("Error setting R as language: {e}"))?; + + let tree = parser + .parse(code, None) + .ok_or(anyhow!("Failed to parse code"))?; + let root_node = tree.root_node(); + + let mut packages = vec![]; + find_library_calls(root_node, code, &mut packages); + + // Deduplicate and exclude base packages + packages.sort(); + packages.dedup(); + packages.retain(|p| !is_base_package(p)); + + Ok(packages.join("\n")) +} + +fn find_library_calls(node: Node, code: &str, packages: &mut Vec) { + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + if child.kind() == "call" { + // call node: child 0 is the function name, child 1 is arguments + if let (Some(func_node), Some(args_node)) = (child.child(0), child.child(1)) { + let func_name = func_node.utf8_text(code.as_bytes()).unwrap_or(""); + if func_name == "library" || func_name == "require" { + // AST: arguments → ( + argument → identifier/string + ) + if args_node.kind() == "arguments" { + let mut args_cursor = args_node.walk(); + for arg in args_node.children(&mut args_cursor) { + if arg.kind() == "argument" { + // The argument node wraps the actual value + if let Some(value_node) = arg.child(0) { + let pkg = value_node + .utf8_text(code.as_bytes()) + .unwrap_or("") + .trim_matches('"') + .trim_matches('\''); + if !pkg.is_empty() { + packages.push(pkg.to_string()); + } + } + break; // only first arg + } + } + } + } + } + } + // Recurse into children to find nested library() calls + find_library_calls(child, code, packages); + } +} + +fn is_base_package(pkg: &str) -> bool { + matches!( + pkg, + "base" + | "compiler" + | "datasets" + | "grDevices" + | "graphics" + | "grid" + | "methods" + | "parallel" + | "splines" + | "stats" + | "stats4" + | "tcltk" + | "tools" + | "utils" + ) +} + +/// Find the main function signature in R code. +/// R function definitions look like: `main <- function(x, y = 10) { ... }` +/// In the tree-sitter-r AST, this is a `binary_operator` node with: +/// - child 0: identifier "main" +/// - child 1: "<-" or "=" +/// - child 2: function_definition node +fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> anyhow::Result>> { + let mut cursor = root_node.walk(); + for x in root_node.children(&mut cursor) { + if x.kind() == "binary_operator" { + let child_count = x.child_count(); + if child_count < 3 { + continue; + } + + // First child should be identifier "main" + let ident_node = x.child(0).unwrap(); + if ident_node.kind() != "identifier" { + continue; + } + let ident = ident_node.utf8_text(code.as_bytes()).unwrap_or(""); + if ident != "main" { + continue; + } + + // Second child should be "<-" or "=" + let op_node = x.child(1).unwrap(); + let op = op_node.utf8_text(code.as_bytes()).unwrap_or(""); + if op != "<-" && op != "=" { + continue; + } + + // Third child should be the function_definition + let func_node = x.child(2).unwrap(); + if func_node.kind() != "function_definition" { + continue; + } + + return Ok(Some(parse_function_params(func_node, code)?)); + } + } + Ok(None) +} + +/// Parse parameters from a function_definition node. +/// function_definition has children: "function", parameters, body +/// Each parameter node has: +/// - 1 child (identifier) for positional args +/// - 3 children (identifier, "=", value) for default args +fn parse_function_params(func_node: Node, code: &str) -> anyhow::Result> { + let mut args = vec![]; + let mut func_cursor = func_node.walk(); + + for child in func_node.children(&mut func_cursor) { + if child.kind() == "parameters" { + let mut param_cursor = child.walk(); + for param in child.children(&mut param_cursor) { + if param.kind() != "parameter" { + continue; + } + + let param_child_count = param.child_count(); + if param_child_count == 1 { + // Simple parameter: just identifier + let ident_node = param.child(0).unwrap(); + let name = ident_node.utf8_text(code.as_bytes())?; + args.push(Arg { name: name.to_owned(), ..Default::default() }); + } else if param_child_count >= 3 { + // Default parameter: identifier = value + let ident_node = param.child(0).unwrap(); + let value_node = param.child(2).unwrap(); + let name = ident_node.utf8_text(code.as_bytes())?; + + let Range { start_byte, end_byte, .. } = value_node.range(); + let raw = &code[start_byte..end_byte]; + // Convert R literals to JSON + let unparsed = raw + .replace("NULL", "null") + .replace("TRUE", "true") + .replace("FALSE", "false"); + match serde_json::from_str::(&unparsed) { + Ok(default) => { + args.push(Arg { + name: name.to_owned(), + typ: json_to_typ(&default, true), + default: Some(default), + has_default: true, + ..Default::default() + }); + } + Err(_) => { + args.push(Arg { + name: name.to_owned(), + has_default: true, + ..Default::default() + }); + } + } + } + } + } + } + Ok(args) +} + +#[cfg(test)] +mod test { + use serde_json::json; + use windmill_parser::Typ; + + use super::parse_r_sig_meta as parse; + + #[test] + fn test_parse_r_no_main() { + let code = r#" +not_main <- function() {} +helper <- function(x) { x + 1 } +"#; + let sig = parse(code).unwrap(); + assert_eq!( + sig, + windmill_parser::MainArgSignature { auto_kind: None, ..Default::default() } + ); + } + + #[test] + fn test_parse_r_no_args() { + let code = r#" +main <- function() { + return(42) +} +"#; + let sig = parse(code).unwrap(); + assert_eq!( + sig, + windmill_parser::MainArgSignature { auto_kind: None, ..Default::default() } + ); + } + + #[test] + fn test_parse_r_positional_args() { + let code = r#"main <- function(a, b, c) { a + b + c }"#; + let sig = parse(code).unwrap(); + assert_eq!( + sig, + windmill_parser::MainArgSignature { + args: vec![ + windmill_parser::Arg { name: "a".into(), ..Default::default() }, + windmill_parser::Arg { name: "b".into(), ..Default::default() }, + windmill_parser::Arg { name: "c".into(), ..Default::default() }, + ], + auto_kind: None, + ..Default::default() + } + ); + } + + #[test] + fn test_parse_r_default_args() { + let code = r#"main <- function(a = 10, b = "hey", c = FALSE) { }"#; + let sig = parse(code).unwrap(); + assert_eq!(sig.args.len(), 3); + assert_eq!(sig.args[0].name, "a"); + assert_eq!(sig.args[0].default, Some(json!(10))); + assert_eq!(sig.args[0].typ, Typ::Int); + assert_eq!(sig.args[1].name, "b"); + assert_eq!(sig.args[1].default, Some(json!("hey"))); + assert_eq!(sig.args[1].typ, Typ::Str(None)); + assert_eq!(sig.args[2].name, "c"); + assert_eq!(sig.args[2].default, Some(json!(false))); + assert_eq!(sig.args[2].typ, Typ::Bool); + } + + #[test] + fn test_parse_r_equals_assignment() { + let code = r#"main = function(x, y = 5) { x + y }"#; + let sig = parse(code).unwrap(); + assert_eq!(sig.args.len(), 2); + assert_eq!(sig.args[0].name, "x"); + assert_eq!(sig.args[1].name, "y"); + assert_eq!(sig.args[1].default, Some(json!(5))); + } + + #[test] + fn test_parse_r_null_default() { + let code = r#"main <- function(x = NULL) { x }"#; + let sig = parse(code).unwrap(); + assert_eq!(sig.args.len(), 1); + assert_eq!(sig.args[0].name, "x"); + assert_eq!(sig.args[0].default, Some(json!(null))); + } + + #[test] + fn test_parse_r_requirements() { + use super::parse_r_requirements; + + let code = r#" +library(dplyr) +library(ggplot2) +require(tidyr) +library(stats) + +main <- function(x) { + library(stringr) + x +} +"#; + let reqs = parse_r_requirements(code).unwrap(); + let pkgs: Vec<&str> = reqs.lines().collect(); + assert!(pkgs.contains(&"dplyr")); + assert!(pkgs.contains(&"ggplot2")); + assert!(pkgs.contains(&"tidyr")); + assert!(pkgs.contains(&"stringr")); + assert!(!pkgs.contains(&"stats")); // base package excluded + } + + #[test] + fn test_parse_r_requirements_string_args() { + use super::parse_r_requirements; + + let code = r#" +library("data.table") +require("jsonlite") + +main <- function() { } +"#; + let reqs = parse_r_requirements(code).unwrap(); + let pkgs: Vec<&str> = reqs.lines().collect(); + assert!(pkgs.contains(&"data.table")); + assert!(pkgs.contains(&"jsonlite")); + } + + #[test] + fn test_parse_r_requirements_no_deps() { + use super::parse_r_requirements; + + let code = r#"main <- function(x) { x + 1 }"#; + let reqs = parse_r_requirements(code).unwrap(); + assert!(reqs.is_empty()); + } +} diff --git a/backend/parsers/windmill-parser-r/src/wasm_libc.rs b/backend/parsers/windmill-parser-r/src/wasm_libc.rs new file mode 100644 index 0000000000..924748a073 --- /dev/null +++ b/backend/parsers/windmill-parser-r/src/wasm_libc.rs @@ -0,0 +1,293 @@ +use std::collections::BTreeMap; +use std::sync::{Mutex, OnceLock}; +use std::{ + alloc::{self, Layout}, + ffi::{c_char, c_int, c_void}, + mem::align_of, + ptr, +}; +use wasm_bindgen::prelude::*; + +/* -------------------------------- stdlib.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn abort() { + panic!("Aborted from C"); +} + +macro_rules! console_log { + ($($t:tt)*) => (unsafe { log(&format_args!($($t)*).to_string()) }) +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(a: &str); +} + +#[no_mangle] +pub unsafe extern "C" fn malloc(size: usize) -> *mut c_void { + if size == 0 { + return ptr::null_mut(); + } + + let (layout, offset_to_data) = layout_for_size_prepended(size); + let buf = alloc::alloc(layout); + store_layout(buf, layout, offset_to_data) +} + +#[no_mangle] +pub unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut c_void { + if count == 0 || size == 0 { + return ptr::null_mut(); + } + + let (layout, offset_to_data) = layout_for_size_prepended(size * count); + let buf = alloc::alloc_zeroed(layout); + store_layout(buf, layout, offset_to_data) +} + +#[no_mangle] +pub unsafe extern "C" fn realloc(buf: *mut c_void, new_size: usize) -> *mut c_void { + if buf.is_null() { + malloc(new_size) + } else if new_size == 0 { + free(buf); + ptr::null_mut() + } else { + let (old_buf, old_layout) = retrieve_layout(buf); + let (new_layout, offset_to_data) = layout_for_size_prepended(new_size); + let new_buf = alloc::realloc(old_buf, old_layout, new_layout.size()); + store_layout(new_buf, new_layout, offset_to_data) + } +} + +#[no_mangle] +pub unsafe extern "C" fn free(buf: *mut c_void) { + if buf.is_null() { + return; + } + let (buf, layout) = retrieve_layout(buf); + alloc::dealloc(buf, layout); +} + +// In all these allocations, we store the layout before the data for later retrieval. +// This is because we need to know the layout when deallocating the memory. +// Here are some helper methods for that: + +/// Given a pointer to the data, retrieve the layout and the pointer to the layout. +unsafe fn retrieve_layout(buf: *mut c_void) -> (*mut u8, Layout) { + let (_, layout_offset) = Layout::new::() + .extend(Layout::from_size_align(0, align_of::<*const u8>() * 2).unwrap()) + .unwrap(); + + let buf = (buf as *mut u8).offset(-(layout_offset as isize)); + let layout = *(buf as *mut Layout); + + (buf, layout) +} + +/// Calculate a layout for a given size with space for storing a layout at the start. +/// Returns the layout and the offset to the data. +fn layout_for_size_prepended(size: usize) -> (Layout, usize) { + Layout::new::() + .extend(Layout::from_size_align(size, align_of::<*const u8>() * 2).unwrap()) + .unwrap() +} + +/// Store a layout in the pointer, returning a pointer to where the data should be stored. +unsafe fn store_layout(buf: *mut u8, layout: Layout, offset_to_data: usize) -> *mut c_void { + *(buf as *mut Layout) = layout; + (buf as *mut u8).offset(offset_to_data as isize) as *mut c_void +} + +/* -------------------------------- string.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn strncmp(ptr1: *const c_void, ptr2: *const c_void, n: usize) -> c_int { + let s1 = std::slice::from_raw_parts(ptr1 as *const u8, n); + let s2 = std::slice::from_raw_parts(ptr2 as *const u8, n); + + for (a, b) in s1.iter().zip(s2.iter()) { + if *a != *b || *a == 0 { + return (*a as i32) - (*b as i32); + } + } + + 0 +} + +// Implementation by AI: +pub type size_t = usize; +use std::slice; +#[no_mangle] +pub unsafe extern "C" fn memchr(haystack: *const c_void, needle: c_int, len: usize) -> *mut c_void { + if haystack.is_null() || len == 0 { + return ptr::null_mut(); // Return null if the input pointer is null or length is zero + } + + let needle_byte = needle as u8; // Convert needle to a byte + + // Create a pointer to the start of the haystack + let mut current = haystack as *const u8; + + // Iterate through the memory block + for _ in 0..len { + if *current == needle_byte { + return current as *mut c_void; // Return the pointer to the found byte + } + current = current.add(1); // Move to the next byte + } + + ptr::null_mut() // Return null if the byte was not found +} + +#[no_mangle] +pub unsafe extern "C" fn strchr(mut s: *const c_char, c: c_int) -> *mut c_char { + if s.is_null() { + return std::ptr::null_mut(); // Return null if the input string is null + } + + let target = c as u8 as char; // Convert c to a char + let mut current = s; + + // Iterate through the string until we find the character or reach the end + while *current != 0 { + if *current as u8 as char == target { + return current as *mut c_char; // Return the pointer to the found character + } + current = current.add(1); // Move to the next character + } + + std::ptr::null_mut() // Return null if the character was not found +} +// End of AI implemetation +/* -------------------------------- wctype.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn iswspace(c: c_int) -> bool { + char::from_u32(c as u32).map_or(false, |c| c.is_whitespace()) +} + +#[no_mangle] +pub unsafe extern "C" fn iswalnum(c: c_int) -> bool { + char::from_u32(c as u32).map_or(false, |c| c.is_alphanumeric()) +} + +// Implementation by AI: +pub type wint_t = u32; + +#[no_mangle] +pub extern "C" fn iswdigit(wc: wint_t) -> c_int { + // Check if the character is a digit ('0' to '9') + if wc >= '0' as wint_t && wc <= '9' as wint_t { + return 1; // Return true (1) + } + 0 // Return false (0) +} + +#[no_mangle] +pub extern "C" fn iswupper(wc: wint_t) -> c_int { + // Check if the character is an uppercase letter ('A' to 'Z') + if wc >= 'A' as wint_t && wc <= 'Z' as wint_t { + return 1; // Return true (1) + } + 0 // Return false (0) +} + +#[no_mangle] +pub extern "C" fn iswalpha(wc: wint_t) -> c_int { + // Check if the character is an alphabetic character ('A' to 'Z' or 'a' to 'z') + if (wc >= 'A' as wint_t && wc <= 'Z' as wint_t) || (wc >= 'a' as wint_t && wc <= 'z' as wint_t) + { + return 1; // Return true (1) + } + 0 // Return false (0) +} + +#[no_mangle] +pub extern "C" fn iswlower(wc: wint_t) -> c_int { + // Check if the character is a lowercase letter ('a' to 'z') + if wc >= 'a' as wint_t && wc <= 'z' as wint_t { + return 1; // Return true (1) + } + 0 // Return false (0) +} +// End of AI implemetation + +/* --------------------------------- time.h --------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn clock() -> u64 { + panic!("clock is not supported"); +} + +/* --------------------------------- ctype.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn isprint(c: c_int) -> bool { + c >= 32 && c <= 126 +} + +/* --------------------------------- stdio.h -------------------------------- */ + +#[no_mangle] +pub unsafe extern "C" fn fprintf(_file: *mut c_void, _format: *const c_void, _args: ...) -> c_int { + panic!("fprintf is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fputs(_s: *const c_void, _file: *mut c_void) -> c_int { + panic!("fputs is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fputc(_c: c_int, _file: *mut c_void) -> c_int { + panic!("fputc is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fdopen(_fd: c_int, _mode: *const c_void) -> *mut c_void { + panic!("fdopen is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fclose(_file: *mut c_void) -> c_int { + panic!("fclose is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn fwrite( + _ptr: *const c_void, + _size: usize, + _nmemb: usize, + _stream: *mut c_void, +) -> usize { + panic!("fwrite is not supported"); +} + +#[no_mangle] +pub unsafe extern "C" fn vsnprintf( + _buf: *mut c_char, + _size: usize, + _format: *const c_char, + _args: ... +) -> c_int { + panic!("vsnprintf is not supported"); +} + +#[no_mangle] +pub extern "C" fn clock_gettime(ptr: usize, new_size: usize) { + panic!("clock_gettime is not supported"); +} + +// int snprintf( char* restrict buffer, size_t bufsz, const char* restrict format, ... ); +#[no_mangle] +pub extern "C" fn snprintf() { + panic!("snprintf is not supported"); +} + +#[no_mangle] +pub extern "C" fn __assert_fail(_: *const i32, _: *const i32, _: *const i32, _: *const i32) { + panic!("oh no"); +} diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 895a35713e..edc19c4a29 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -38,6 +38,7 @@ csharp-parser = [ "dep:windmill-parser-csharp"] nu-parser = [ "dep:windmill-parser-nu"] java-parser = [ "dep:windmill-parser-java"] ruby-parser = [ "dep:windmill-parser-ruby"] +r-parser = [ "dep:windmill-parser-r"] wac-parser = [ "dep:windmill-parser-wac"] asset-parser = [ "dep:windmill-parser-ts-asset", "dep:windmill-parser-py-asset", "dep:windmill-parser-sql-asset"] py-imports-parser = [ "dep:windmill-parser-py-imports"] @@ -58,6 +59,7 @@ windmill-parser-csharp = { workspace = true, optional = true } windmill-parser-nu = { workspace = true, optional = true } windmill-parser-java = { workspace = true, optional = true } windmill-parser-ruby = { workspace = true, optional = true } +windmill-parser-r = { workspace = true, optional = true } windmill-parser-wac = { workspace = true, optional = true } windmill-parser-ts-asset = { workspace = true, optional = true } windmill-parser-py-asset = { workspace = true, optional = true } diff --git a/backend/parsers/windmill-parser-wasm/build.nu b/backend/parsers/windmill-parser-wasm/build.nu index 212c5f3a37..2df321e374 100755 --- a/backend/parsers/windmill-parser-wasm/build.nu +++ b/backend/parsers/windmill-parser-wasm/build.nu @@ -55,6 +55,11 @@ const targets = [ desc: "Ruby", features: "ruby-parser", env: "tree-sitter", + }, { + ident: "r", + desc: "R", + features: "r-parser", + env: "tree-sitter", }, { ident: "wac", diff --git a/backend/parsers/windmill-parser-wasm/src/lib.rs b/backend/parsers/windmill-parser-wasm/src/lib.rs index a2cef1b0ef..c843652e26 100644 --- a/backend/parsers/windmill-parser-wasm/src/lib.rs +++ b/backend/parsers/windmill-parser-wasm/src/lib.rs @@ -198,6 +198,12 @@ pub fn parse_ruby(code: &str) -> String { wrap_sig(windmill_parser_ruby::parse_ruby_signature(code)) } +#[cfg(feature = "r-parser")] +#[wasm_bindgen] +pub fn parse_r(code: &str) -> String { + wrap_sig(windmill_parser_r::parse_r_signature(code)) +} + #[cfg(feature = "asset-parser")] #[wasm_bindgen] pub fn parse_assets_sql(code: &str) -> String { diff --git a/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h b/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h index 30fce92495..4a2abf1142 100644 --- a/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h +++ b/backend/parsers/windmill-parser-wasm/wasm-sysroot/string.h @@ -1,5 +1,7 @@ #pragma once +#include + void *memcpy(void *dest, const void *src, unsigned long n); void *memmove(void *dest, const void *src, unsigned long n); void *memset(void *s, int c, unsigned long n); diff --git a/backend/src/main.rs b/backend/src/main.rs index 2ac71aba57..12a6616c51 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -95,7 +95,7 @@ use windmill_worker::{ BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, CSHARP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR, JAVA_CACHE_DIR, NU_CACHE_DIR, POWERSHELL_CACHE_DIR, PY310_CACHE_DIR, PY311_CACHE_DIR, PY312_CACHE_DIR, PY313_CACHE_DIR, - RUBY_CACHE_DIR, RUST_CACHE_DIR, TAR_JAVA_CACHE_DIR, UV_CACHE_DIR, + RUBY_CACHE_DIR, RUST_CACHE_DIR, R_CACHE_DIR, TAR_JAVA_CACHE_DIR, UV_CACHE_DIR, }; use crate::monitor::{ @@ -2011,6 +2011,7 @@ pub async fn run_workers( &*POWERSHELL_CACHE_DIR, &*JAVA_CACHE_DIR, &*RUBY_CACHE_DIR, + &*R_CACHE_DIR, &*TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG ] { DirBuilder::new() diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index 481ff43ea7..582bf7b684 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -26,7 +26,7 @@ native_trigger_service: nextcloud request_type: sync, async, sync_sse runnable_type: ScriptHash, ScriptPath, FlowPath script_kind: script, trigger, failure, command, approval, preprocessor -script_lang: python3, deno, go, bash, postgresql, nativets, bun, mysql, bigquery, snowflake, graphql, powershell, mssql, php, bunnative, rust, ansible, csharp, oracledb, nu, java, duckdb, ruby +script_lang: python3, deno, go, bash, postgresql, nativets, bun, mysql, bigquery, snowflake, graphql, powershell, mssql, php, bunnative, rust, ansible, csharp, oracledb, nu, java, duckdb, ruby, rlang trigger_kind: webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt, gcp, default_email, nextcloud trigger_mode: enabled, disabled, suspended workspace_key_kind: cloud diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 254ab33153..61f7289c0e 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -1081,6 +1081,115 @@ echo "$result" Ok(()) } +#[cfg(feature = "rlang")] +#[sqlx::test(fixtures("base"))] +async fn test_r_job(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let content = r#" +main <- function(msg) { + return(paste("hello", msg)) +} +"# + .to_owned(); + + let result = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + lock: None, + language: ScriptLang::Rlang, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + })) + .arg("msg", json!("world")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + assert_eq!(result, json!("hello world")); + Ok(()) +} + +#[cfg(feature = "rlang")] +#[sqlx::test(fixtures("base", "wmill_cli_test"))] +async fn test_r_get_variable(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let content = r#" +main <- function() { + return(get_variable("u/test-user/test_var")) +} +"# + .to_owned(); + + let result = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + lock: None, + language: ScriptLang::Rlang, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + })) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + assert_eq!(result, json!("hello from variable")); + Ok(()) +} + +#[cfg(feature = "rlang")] +#[sqlx::test(fixtures("base", "wmill_cli_test"))] +async fn test_r_get_resource(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let content = r#" +main <- function() { + return(get_resource("u/test-user/test_res")) +} +"# + .to_owned(); + + let result = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content, + path: None, + lock: None, + language: ScriptLang::Rlang, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + })) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + assert_eq!(result, json!({"host": "localhost", "port": 5432})); + Ok(()) +} + #[cfg(feature = "nu")] #[sqlx::test(fixtures("base"))] async fn test_nu_job(db: Pool) -> anyhow::Result<()> { diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index a909fc0ffc..afa5ded9dc 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -820,6 +820,7 @@ async fn create_script_internal<'c>( || ns.language == ScriptLang::Php || ns.language == ScriptLang::Java || ns.language == ScriptLang::Ruby + || ns.language == ScriptLang::Rlang // for related places search: ADD_NEW_LANG ) { Some(String::new()) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 78624fd5f9..35a73bd1ce 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -20705,6 +20705,7 @@ components: nu, java, ruby, + rlang, duckdb, bunnative, # for related places search: ADD_NEW_LANG diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 6c6c783ba7..d6b5e35e98 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -484,6 +484,7 @@ pub(crate) async fn tarball_workspace( ScriptLang::OracleDB => "odb.sql", ScriptLang::Java => "java", ScriptLang::Ruby => "rb", + ScriptLang::Rlang => "r", // for related places search: ADD_NEW_LANG }; archive diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 62a4bdc700..95e41c3bcd 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -100,6 +100,7 @@ pub const ENV_SETTINGS: &[&str] = &[ "BUNDLE_PATH", "GEM_PATH", "RUBY_CONCURRENT_DOWNLOADS", + "RSCRIPT_PATH", // for related places search: ADD_NEW_LANG "GOPRIVATE", "GOPROXY", diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index 3023a5ab33..ac1fe24a4e 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -597,6 +597,7 @@ pub enum ScriptLang { Nu, Java, Ruby, + Rlang, } // --------------------------------------------------------------------------- diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 026f60da75..7a200fb44b 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -184,6 +184,7 @@ lazy_static::lazy_static! { "nu".to_string(), "java".to_string(), "ruby".to_string(), + "rlang".to_string(), "duckdb".to_string(), // for related places search: ADD_NEW_LANG "dependency".to_string(), @@ -728,6 +729,13 @@ pub struct RubyAnnotations { pub verbose: bool, } +#[annotations("#")] +pub struct RlangAnnotations { + pub renv_verbose: bool, + pub renv_install_verbose: bool, + pub sandbox: bool, +} + #[annotations("#")] pub struct PythonAnnotations { pub no_cache: bool, diff --git a/backend/windmill-runtime-nativets/src/windmill-client.js b/backend/windmill-runtime-nativets/src/windmill-client.js index 24e781343e..b0816dd78c 100644 --- a/backend/windmill-runtime-nativets/src/windmill-client.js +++ b/backend/windmill-runtime-nativets/src/windmill-client.js @@ -3000,6 +3000,7 @@ var $RawScript = { "nativets", "duckdb", "ruby", + "rlang", // for related places search: ADD_NEW_LANG ], }, diff --git a/backend/windmill-types/src/scripts.rs b/backend/windmill-types/src/scripts.rs index e5a980eaba..af3a2a9183 100644 --- a/backend/windmill-types/src/scripts.rs +++ b/backend/windmill-types/src/scripts.rs @@ -65,6 +65,7 @@ pub enum ScriptLang { Nu, Java, Ruby, + Rlang, // for related places search: ADD_NEW_LANG } @@ -94,6 +95,7 @@ impl ScriptLang { ScriptLang::Nu => "nu", ScriptLang::Java => "java", ScriptLang::Ruby => "ruby", + ScriptLang::Rlang => "rlang", // for related places search: ADD_NEW_LANG } } @@ -132,7 +134,7 @@ impl ScriptLang { use ScriptLang::*; match self { Nativets | Bun | Bunnative | Deno | Go | Php | CSharp | Java => "//", - Python3 | Bash | Powershell | Graphql | Ansible | Nu | Ruby => "#", + Python3 | Bash | Powershell | Graphql | Ansible | Nu | Ruby | Rlang => "#", Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB | DuckDb => "--", Rust => "//!", // for related places search: ADD_NEW_LANG @@ -167,6 +169,7 @@ impl FromStr for ScriptLang { "nu" => ScriptLang::Nu, "java" => ScriptLang::Java, "ruby" => ScriptLang::Ruby, + "rlang" => ScriptLang::Rlang, // for related places search: ADD_NEW_LANG language => return Err(anyhow::anyhow!("{} is currently not supported", language)), }; diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index 8955041435..4e2357b14e 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -36,6 +36,7 @@ rust = ["dep:windmill-parser-rust"] nu = ["dep:windmill-parser-nu"] java = ["dep:windmill-parser-java"] ruby = ["dep:windmill-parser-ruby"] +rlang = ["dep:windmill-parser-r"] duckdb = ["dep:libloading"] quickjs = ["windmill-jseval/quickjs"] bedrock = ["dep:aws-sdk-bedrockruntime", "windmill-common/bedrock", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-types"] @@ -60,6 +61,7 @@ windmill-parser-csharp = { workspace = true, optional = true } windmill-parser-nu = { workspace = true, optional = true } windmill-parser-java = { workspace = true, optional = true } windmill-parser-ruby = { workspace = true, optional = true } +windmill-parser-r = { workspace = true, optional = true } windmill-parser-py = { workspace = true, optional = true } windmill-parser-yaml.workspace = true windmill-parser-py-imports = { workspace = true, optional = true } diff --git a/backend/windmill-worker/nsjail/install.r.config.proto b/backend/windmill-worker/nsjail/install.r.config.proto new file mode 100644 index 0000000000..8163be8bbb --- /dev/null +++ b/backend/windmill-worker/nsjail/install.r.config.proto @@ -0,0 +1,100 @@ +name: "r install" + +mode: ONCE +hostname: "r" +log_level: ERROR +time_limit: 900 + +disable_rl: true + +envar: "HOME=/tmp" +envar: "R_INSTALL_TAR=/usr/bin/tar --no-same-owner" + +cwd: "/tmp" + +clone_newnet: false +clone_newuser: {CLONE_NEWUSER} + +skip_setsid: true +keep_caps: true +keep_env: true +mount_proc: true + + +mount { + src: "/bin" + dst: "/bin" + is_bind: true + mandatory: false +} + +mount { + src: "/lib" + dst: "/lib" + is_bind: true + mandatory: false +} + +mount { + src: "/lib64" + dst: "/lib64" + is_bind: true + mandatory: false +} + +mount { + src: "/usr" + dst: "/usr" + is_bind: true + mandatory: false +} + +mount { + src: "/etc" + dst: "/etc" + is_bind: true +} + +mount { + src: "/dev/null" + dst: "/dev/null" + is_bind: true + rw: true +} + +mount { + src: "{JOB_DIR}" + dst: "/tmp" + is_bind: true + mandatory: false + rw: true +} + +mount { + src: "{PKG_DIR}" + dst: "/install" + is_bind: true + rw: true +} + +mount { + src: "/sys/devices/system/cpu" + dst: "/sys/devices/system/cpu" + is_bind: true + mandatory: false +} + +mount { + src: "/dev/urandom" + dst: "/dev/urandom" + is_bind: true +} + +mount { + src: "{TRACING_PROXY_CA_CERT_PATH}" + dst: "{TRACING_PROXY_CA_CERT_PATH}" + is_bind: true + mandatory: false +} + +#{DEV} diff --git a/backend/windmill-worker/nsjail/run.r.config.proto b/backend/windmill-worker/nsjail/run.r.config.proto new file mode 100644 index 0000000000..72c30f489b --- /dev/null +++ b/backend/windmill-worker/nsjail/run.r.config.proto @@ -0,0 +1,125 @@ +name: "r run script" + +mode: ONCE +hostname: "r" +log_level: ERROR + +disable_rl: true + +cwd: "/tmp" + +clone_newnet: false +clone_newuser: {CLONE_NEWUSER} + +skip_setsid: true +keep_caps: false +keep_env: true +# mount_proc: true + +mount { + src: "/bin" + dst: "/bin" + is_bind: true + mandatory: false +} + +mount { + src: "/lib" + dst: "/lib" + is_bind: true + mandatory: false +} + +mount { + src: "/lib64" + dst: "/lib64" + is_bind: true + mandatory: false +} + +mount { + src: "/usr" + dst: "/usr" + is_bind: true + mandatory: false +} + +mount { + src: "/dev/null" + dst: "/dev/null" + is_bind: true + rw: true +} + +mount { + dst: "/tmp" + fstype: "tmpfs" + rw: true + options: "size=500000000" +} + + +mount { + src: "{JOB_DIR}/main.r" + dst: "/tmp/main.r" + is_bind: true + mandatory: false +} + +mount { + src: "{JOB_DIR}/args.json" + dst: "/tmp/args.json" + is_bind: true +} + +mount { + src: "{JOB_DIR}/result.json" + dst: "/tmp/result.json" + rw: true + is_bind: true +} + +mount { + src: "{R_CACHE_DIR}" + dst: "{R_CACHE_DIR}" + is_bind: true + mandatory: false +} + +mount { + src: "/etc" + dst: "/etc" + is_bind: true +} + +mount { + src: "/sys/devices/system/cpu" + dst: "/sys/devices/system/cpu" + is_bind: true + mandatory: false +} + +mount { + src: "/dev/random" + dst: "/dev/random" + is_bind: true +} + +mount { + src: "/dev/urandom" + dst: "/dev/urandom" + is_bind: true +} + +iface_no_lo: true + +{SHARED_MOUNT} + +mount { + src: "{TRACING_PROXY_CA_CERT_PATH}" + dst: "{TRACING_PROXY_CA_CERT_PATH}" + is_bind: true + mandatory: false +} + +#{DEV} diff --git a/backend/windmill-worker/src/lib.rs b/backend/windmill-worker/src/lib.rs index e97ec8ff9a..bb32373b76 100644 --- a/backend/windmill-worker/src/lib.rs +++ b/backend/windmill-worker/src/lib.rs @@ -17,6 +17,9 @@ mod java_executor; #[cfg(feature = "ruby")] mod ruby_executor; +#[cfg(feature = "rlang")] +mod r_executor; + mod ai; mod ai_executor; mod bun_executor; diff --git a/backend/windmill-worker/src/r_executor.rs b/backend/windmill-worker/src/r_executor.rs new file mode 100644 index 0000000000..fa8a1809b1 --- /dev/null +++ b/backend/windmill-worker/src/r_executor.rs @@ -0,0 +1,715 @@ +use std::{collections::HashMap, process::Stdio}; + +use itertools::Itertools; +use tokio::{ + fs::{self, File}, + io::{AsyncReadExt, AsyncWriteExt}, + process::Command, +}; +use uuid::Uuid; +use windmill_common::{ + client::AuthedClient, + error::Error, + utils::calculate_hash, + worker::{write_file, Connection, RlangAnnotations}, +}; +use windmill_parser::Arg; +use windmill_parser_r::{parse_r_requirements, parse_r_signature}; +use windmill_queue::{append_logs, CanceledBy, MiniPulledJob}; + +use crate::{ + common::{ + build_command_with_isolation, create_args_and_out_file, get_reserved_variables, + read_result, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL, + }, + get_proxy_envs_for_lang, + handle_child::{self}, + is_sandboxing_enabled, + universal_pkg_installer::{ + par_install_language_dependencies_seq, DependencyGraph, InstallDeps, RequiredDependency, + }, + DISABLE_NUSER, NSJAIL_AVAILABLE, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, R_CACHE_DIR, + TRACING_PROXY_CA_CERT_PATH, +}; +use windmill_common::scripts::ScriptLang; + +lazy_static::lazy_static! { + static ref RSCRIPT_PATH: String = std::env::var("RSCRIPT_PATH").unwrap_or_else(|_| "/usr/bin/Rscript".to_string()); + static ref R_CONCURRENT_DOWNLOADS: usize = std::env::var("R_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(5)).unwrap_or(5); + static ref R_PROXY_ENVS: Vec<(String, String)> = { + PROXY_ENVS + .clone() + .into_iter() + .map(|(k, v)| (k.to_lowercase(), v)) + .collect() + }; +} + +const NSJAIL_CONFIG_RUN_R_CONTENT: &str = include_str!("../nsjail/run.r.config.proto"); +const NSJAIL_CONFIG_INSTALL_R_CONTENT: &str = include_str!("../nsjail/install.r.config.proto"); + +#[allow(dead_code)] +pub(crate) struct JobHandlerInput<'a> { + pub base_internal_url: &'a str, + pub canceled_by: &'a mut Option, + pub client: &'a AuthedClient, + pub parent_runnable_path: Option, + pub conn: &'a Connection, + pub envs: HashMap, + pub inner_content: &'a str, + pub job: &'a MiniPulledJob, + pub job_dir: &'a str, + pub mem_peak: &'a mut i32, + pub occupancy_metrics: &'a mut OccupancyMetrics, + pub requirements_o: Option<&'a String>, + pub shared_mount: &'a str, + pub worker_name: &'a str, +} + +pub async fn handle_r_job<'a>( + mut args: JobHandlerInput<'a>, +) -> Result, Error> { + let annotation = RlangAnnotations::parse(args.inner_content); + + if !std::path::Path::new(RSCRIPT_PATH.as_str()).exists() { + return Err(Error::ExecutionErr(format!( + "Rscript binary not found at '{}'. R is only available in the windmill-full (CE) or windmill-ee-full (EE) Docker images.", + *RSCRIPT_PATH + ))); + } + + if annotation.sandbox && NSJAIL_AVAILABLE.is_none() { + return Err(Error::ExecutionErr( + "Script has #sandbox annotation but nsjail is not available on this worker. \ + Please ensure nsjail is installed or remove the #sandbox annotation." + .to_string(), + )); + } + + // --- Prepare --- + { + prepare(&args).await?; + } + // --- Resolve lockfile --- + let lockfile = resolve( + &args.job.id, + args.inner_content, + args.mem_peak, + args.canceled_by, + args.job_dir, + args.conn, + args.worker_name, + &args.job.workspace_id, + annotation.renv_verbose, + ) + .await?; + // --- Install --- + let lib_path = if !lockfile.is_empty() { + Some( + install( + &mut args, + &lockfile, + annotation.renv_verbose, + annotation.renv_install_verbose, + ) + .await?, + ) + } else { + None + }; + // --- Execute --- + { + run(&mut args, lib_path.as_deref(), annotation.sandbox).await?; + } + // --- Retrieve results --- + { + read_result(&args.job_dir, None).await + } +} + +pub async fn prepare<'a>( + JobHandlerInput { job, conn, job_dir, inner_content, client, .. }: &JobHandlerInput<'a>, +) -> Result<(), Error> { + create_args_and_out_file(&client, job, job_dir, conn).await?; + File::create(format!("{}/main.r", job_dir)) + .await? + .write_all(&wrap(inner_content)?.into_bytes()) + .await?; + + // Create windmill client library for R + let wm_lib_path = format!("{}/r_libs", *R_CACHE_DIR); + fs::create_dir_all(&wm_lib_path).await?; + { + File::create(format!("{}/windmill.r", &wm_lib_path)) + .await? + .write_all( + r##" +# Windmill mini client methods for R +# Uses base R url() + readLines() to avoid requiring any extra R packages + +.wm_fetch_raw <- function(url) { + token <- Sys.getenv("WM_TOKEN") + con <- url(url, headers = c(Authorization = paste("Bearer", token))) + on.exit(close(con)) + paste(readLines(con, warn = FALSE), collapse = "\n") +} + +get_variable <- function(path) { + base_url <- Sys.getenv("BASE_INTERNAL_URL") + workspace <- Sys.getenv("WM_WORKSPACE") + url <- paste0(base_url, "/api/w/", workspace, "/variables/get_value/", path) + jsonlite::fromJSON(.wm_fetch_raw(url)) +} + +get_resource <- function(path) { + base_url <- Sys.getenv("BASE_INTERNAL_URL") + workspace <- Sys.getenv("WM_WORKSPACE") + url <- paste0(base_url, "/api/w/", workspace, "/resources/get_value_interpolated/", path) + jsonlite::fromJSON(.wm_fetch_raw(url)) +} +"## + .as_bytes(), + ) + .await?; + } + Ok(()) +} + +pub async fn resolve<'a>( + job_id: &Uuid, + inner_content: &str, + mem_peak: &mut i32, + canceled_by: &mut Option, + job_dir: &str, + conn: &Connection, + worker_name: &str, + w_id: &str, + verbose: bool, +) -> Result { + let mut packages = parse_r_requirements(inner_content)?; + + // jsonlite is always needed by the wrapper for JSON arg parsing and result serialization + let has_jsonlite = packages.lines().any(|l| l.trim() == "jsonlite"); + if !has_jsonlite { + if packages.is_empty() { + packages = "jsonlite".to_string(); + } else { + packages.push_str("\njsonlite"); + } + } + + // Check cache + let req_hash = format!("r-{}", calculate_hash(&packages)); + if let Some(db) = conn.as_sql() { + if let Some(cached) = sqlx::query_scalar!( + "SELECT lockfile FROM pip_resolution_cache WHERE hash = $1", + req_hash + ) + .fetch_optional(db) + .await? + { + return Ok(cached); + } + } + + append_logs( + job_id, + w_id, + format!("\n--- RESOLVING R PACKAGES ---\n"), + conn, + ) + .await; + + // main.r is already written by prepare() and contains the library() calls. + // renv will scan it to detect dependencies. + // Disable renv's own package cache — Windmill manages its own install cache. + let resolve_script = format!( + r#"options( + repos = c(CRAN = "https://cloud.r-project.org"), + renv.verbose = {verbose_r}, + renv.config.cache.enabled = FALSE, + renv.config.restart.enabled = FALSE, + renv.config.synchronized.check = FALSE +) +renv::consent(provided = TRUE) +suppressMessages(renv::init(bare = TRUE, restart = FALSE)) +suppressMessages(renv::install(prompt = FALSE)) +suppressMessages(renv::snapshot(type = "implicit", prompt = FALSE)) +"#, + verbose_r = if verbose { "TRUE" } else { "FALSE" }, + ); + + let mut file = File::create(format!("{}/resolve.r", job_dir)).await?; + file.write_all(resolve_script.as_bytes()).await?; + + let child = { + let renv_root = format!("{}/renv", *R_CACHE_DIR); + let rscript_executable = if cfg!(windows) { + "Rscript.exe" + } else { + RSCRIPT_PATH.as_str() + }; + let mut cmd = Command::new(rscript_executable); + cmd.current_dir(job_dir) + .env("PATH", PATH_ENV.as_str()) + .env("RENV_PATHS_ROOT", &renv_root) + .arg("resolve.r") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + start_child_process(cmd, rscript_executable, false).await? + }; + handle_child::handle_child( + job_id, + conn, + mem_peak, + canceled_by, + child, + false, + worker_name, + w_id, + "r resolve", + None, + false, + &mut None, + None, + None, + ) + .await?; + + let lock_path = format!("{}/renv.lock", job_dir); + let mut lock_file = File::open(&lock_path).await?; + let mut lock = String::new(); + lock_file.read_to_string(&mut lock).await?; + + // Cache the lockfile + if let Some(db) = conn.as_sql() { + sqlx::query!( + "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = EXCLUDED.lockfile", + req_hash, + lock.clone(), + ).fetch_optional(db).await?; + } + + // Log a compact summary instead of the entire renv.lock JSON + let pkg_count = serde_json::from_str::(&lock) + .ok() + .and_then(|v| v.get("Packages")?.as_object().map(|o| o.len())) + .unwrap_or(0); + append_logs( + job_id, + w_id, + format!("resolved {} packages\n", pkg_count), + conn, + ) + .await; + Ok(lock) +} + +struct RenvPackage { + name: String, + version: String, + repo_url: String, + /// Package names from Imports + Depends fields + dependencies: Vec, +} + +/// Parse renv.lock JSON and extract package info including dependency edges. +fn parse_renv_lock(lockfile: &str) -> Result, Error> { + let lock: serde_json::Value = serde_json::from_str(lockfile) + .map_err(|e| Error::ExecutionErr(format!("Failed to parse renv.lock: {}", e)))?; + + // Build repo name -> URL map from R.Repositories + let mut repo_urls: HashMap = HashMap::new(); + if let Some(repos) = lock + .get("R") + .and_then(|r| r.get("Repositories")) + .and_then(|r| r.as_array()) + { + for repo in repos { + if let (Some(name), Some(url)) = ( + repo.get("Name").and_then(|v| v.as_str()), + repo.get("URL").and_then(|v| v.as_str()), + ) { + repo_urls.insert(name.to_string(), url.to_string()); + } + } + } + + let packages = lock + .get("Packages") + .and_then(|p| p.as_object()) + .ok_or_else(|| Error::ExecutionErr("renv.lock missing Packages field".to_string()))?; + + let mut result = vec![]; + for (_name, pkg) in packages { + let pkg_name = pkg + .get("Package") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + let version = pkg + .get("Version") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + let repo_name = pkg + .get("Repository") + .and_then(|v| v.as_str()) + .unwrap_or("CRAN"); + let repo_url = repo_urls + .get(repo_name) + .cloned() + .unwrap_or_else(|| "https://cloud.r-project.org".to_string()); + + let mut dependencies = vec![]; + if let Some(imports) = pkg.get("Imports").and_then(|v| v.as_array()) { + for entry in imports { + if let Some(s) = entry.as_str() { + // Entries look like "cli (>= 3.6.2)" — take just the name + let name = s.split_whitespace().next().unwrap_or(""); + if !name.is_empty() && name != "R" { + dependencies.push(name.to_string()); + } + } + } + } + + // Skip renv itself — it's already loaded and reinstalling it while + // loaded triggers a noisy "Restart your R session" message. + if !pkg_name.is_empty() && !version.is_empty() && pkg_name != "renv" { + result.push(RenvPackage { name: pkg_name, version, repo_url, dependencies }); + } + } + Ok(result) +} + +async fn install<'a>( + args: &mut JobHandlerInput<'a>, + lockfile: &str, + verbose: bool, + install_verbose: bool, +) -> Result { + let lib_path = format!("{}/r_site_library", *R_CACHE_DIR); + fs::create_dir_all(&lib_path).await?; + + let packages = parse_renv_lock(lockfile)?; + if packages.is_empty() { + return Ok(lib_path); + } + + #[derive(Clone, Debug)] + struct RPackagePayload { + pkg: String, + version: String, + #[allow(dead_code)] + repo_url: String, + } + + // Build dependency graph for topological layering + let mut graph = DependencyGraph::new(); + for renv_pkg in &packages { + let handle = format!("{}-{}", renv_pkg.name, renv_pkg.version); + // renv uses staged installation: it builds to a temp dir then rename()s onto + // the target. If the target is a bind mount point, rename fails with + // "target file already exists". We work around this by mounting the parent + // (wrapper) dir at /install so renv can freely create /install/{pkg}/ via rename. + let pkg_outer = format!("{}/{}_outer", lib_path, renv_pkg.name); + let path = format!("{}/{}", pkg_outer, renv_pkg.name); + graph.insert( + renv_pkg.name.clone(), + RequiredDependency { + path, + _s3_handle: handle, + display_name: format!("{} ({})", renv_pkg.name, renv_pkg.version), + custom_payload: RPackagePayload { + pkg: renv_pkg.name.clone(), + version: renv_pkg.version.clone(), + repo_url: renv_pkg.repo_url.clone(), + }, + }, + renv_pkg.dependencies.clone(), + ); + } + + let jailed = !cfg!(windows) && is_sandboxing_enabled(); + let job_dir = args.job_dir.to_owned(); + + par_install_language_dependencies_seq( + InstallDeps::Layered(graph), + "r", + "Rscript", + false, + *R_CONCURRENT_DOWNLOADS, + move |dependency| { + let lib_path_c = lib_path.clone(); + let job_dir = job_dir.clone(); + let pkg_name = &dependency.custom_payload.pkg; + // pkg_outer is the wrapper dir mounted rw at /install inside nsjail. + // renv creates /install/{pkg}/ inside it via staged rename. + let pkg_outer = format!("{}/{}_outer", lib_path_c, pkg_name); + std::fs::create_dir_all(&pkg_outer)?; + + let mut cmd = if jailed { + let nsjail_proto = format!("{}.install.config.proto", Uuid::new_v4()); + let config_content = NSJAIL_CONFIG_INSTALL_R_CONTENT + .replace("{JOB_DIR}", &job_dir) + .replace("{PKG_DIR}", &pkg_outer) + .replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()) + .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH) + .replace("#{DEV}", DEV_CONF_NSJAIL); + let _ = write_file( + &job_dir, + &nsjail_proto, + &config_content, + )?; + let mut cmd = Command::new(NSJAIL_PATH.as_str()); + cmd.args(vec![ + "--config", + &nsjail_proto, + "--", + RSCRIPT_PATH.as_str(), + ]); + cmd + } else { + Command::new(if cfg!(windows) { + "Rscript.exe" + } else { + RSCRIPT_PATH.as_str() + }) + }; + + let verbose_r = if verbose { "TRUE" } else { "FALSE" }; + let install_verbose_r = if install_verbose { "TRUE" } else { "FALSE" }; + let install_lib = if jailed { "/install".to_string() } else { pkg_outer.clone() }; + cmd.env_clear() + .current_dir(&job_dir) + .env("PATH", PATH_ENV.as_str()) + .envs(R_PROXY_ENVS.clone()); + cmd + .args(&[ + "-e", + &format!( + r#"options(renv.verbose = {verbose_r}, renv.config.install.verbose = {install_verbose_r}, renv.config.restart.enabled = FALSE); renv::install("{pkg}@{version}", library = "{lib}", dependencies = FALSE)"#, + verbose_r = verbose_r, + install_verbose_r = install_verbose_r, + pkg = dependency.custom_payload.pkg, + version = dependency.custom_payload.version, + lib = install_lib, + ), + // install.packages fallback (no version pinning): + // &format!( + // r#"install.packages("{pkg}", lib = "{lib}", repos = "{repo}", dependencies = FALSE, quiet = {quiet}, INSTALL_opts = "--no-test-load --no-lock")"#, + // pkg = dependency.custom_payload.pkg, + // lib = install_lib, + // repo = dependency.custom_payload.repo_url, + // quiet = quiet_flag, + // ), + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + Ok(cmd) + }, + None, + &args.job.id, + &args.job.workspace_id, + args.worker_name, + jailed, + args.conn, + ) + .await?; + + Ok(format!("{}/r_site_library", *R_CACHE_DIR)) +} + +/// Build R_LIBS_USER from lib_path by listing *_outer subdirs. +/// Each package wrapper dir ({pkg}_outer) is added so R finds {pkg}_outer/{pkg}/DESCRIPTION. +fn r_libs_user(lib_path: &str) -> String { + std::fs::read_dir(lib_path) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .filter(|e| { + e.file_type().map(|t| t.is_dir()).unwrap_or(false) + && e.file_name().to_string_lossy().ends_with("_outer") + }) + .map(|e| e.path().to_string_lossy().to_string()) + .collect::>() + .join(":") +} + +async fn run<'a>( + JobHandlerInput { + occupancy_metrics, + mem_peak, + canceled_by, + worker_name, + job, + conn, + job_dir, + shared_mount, + client, + envs, + base_internal_url, + parent_runnable_path, + .. + }: &mut JobHandlerInput<'a>, + lib_path: Option<&str>, + sandbox: bool, +) -> Result<(), Error> { + let reserved_variables = + get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?; + + let nsjail = !cfg!(windows) && (is_sandboxing_enabled() || sandbox); + let child = if nsjail { + append_logs( + &job.id, + &job.workspace_id, + "\n--- R CODE EXECUTION (nsjail) ---\n".to_string(), + conn, + ) + .await; + + write_file( + job_dir, + "run.config.proto", + &NSJAIL_CONFIG_RUN_R_CONTENT + .replace("{JOB_DIR}", job_dir) + .replace("{SHARED_MOUNT}", &shared_mount) + .replace("{R_CACHE_DIR}", &*R_CACHE_DIR) + .replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH) + .replace("#{DEV}", DEV_CONF_NSJAIL) + .replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()), + )?; + let mut cmd = Command::new(NSJAIL_PATH.as_str()); + cmd.env_clear() + .current_dir(job_dir) + .env("PATH", PATH_ENV.as_str()) + .env("BASE_INTERNAL_URL", base_internal_url) + .envs(envs) + .envs(reserved_variables) + .envs(R_PROXY_ENVS.clone()) + .envs( + get_proxy_envs_for_lang(&ScriptLang::Rlang, &job.id, &job.workspace_id, conn) + .await?, + ); + if let Some(lp) = lib_path { + cmd.env("R_LIBS_USER", r_libs_user(lp)); + } + cmd.args(vec![ + "--config", + "run.config.proto", + "--", + RSCRIPT_PATH.as_str(), + "main.r", + ]); + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + start_child_process(cmd, NSJAIL_PATH.as_str(), false).await? + } else { + append_logs( + &job.id, + &job.workspace_id, + format!("\n--- R CODE EXECUTION ---\n"), + conn, + ) + .await; + + let rscript_executable = if cfg!(windows) { + "Rscript.exe" + } else { + RSCRIPT_PATH.as_str() + }; + + let args = vec!["main.r"]; + let mut cmd = build_command_with_isolation(rscript_executable, &args); + + cmd.env_clear() + .current_dir(job_dir.to_owned()) + .env("PATH", PATH_ENV.as_str()) + .env("BASE_INTERNAL_URL", base_internal_url) + .envs(reserved_variables) + .envs(R_PROXY_ENVS.clone()) + .envs( + get_proxy_envs_for_lang(&ScriptLang::Rlang, &job.id, &job.workspace_id, conn) + .await?, + ) + .envs(envs); + if let Some(lp) = lib_path { + cmd.env("R_LIBS_USER", r_libs_user(lp)); + } + + cmd.stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + #[cfg(windows)] + { + cmd.env("SystemRoot", crate::SYSTEM_ROOT.as_str()) + .env("USERPROFILE", crate::USERPROFILE_ENV.as_str()) + .env( + "TMP", + std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")), + ); + } + start_child_process(cmd, rscript_executable, false).await? + }; + handle_child::handle_child( + &job.id, + conn, + mem_peak, + canceled_by, + child, + nsjail, + worker_name, + &job.workspace_id, + "r", + job.timeout, + false, + &mut Some(occupancy_metrics), + None, + None, + ) + .await?; + Ok(()) +} + +fn wrap(inner_content: &str) -> Result { + let sig = parse_r_signature(inner_content)?; + let spread = sig + .args + .clone() + .into_iter() + .map(|Arg { name, .. }| format!("{name} = args${name}", name = name)) + .collect_vec() + .join(", "); + let wm_lib_path = format!("{}/r_libs/windmill.r", *R_CACHE_DIR); + Ok(format!( + r#"source("{wm_lib_path}") + +suppressPackageStartupMessages({{ +{inner_content} +}}) + +library(jsonlite) +args <- fromJSON("args.json") + +tryCatch({{ + res <- main({spread}) + write(toJSON(res, auto_unbox = TRUE, null = "null"), "result.json") +}}, error = function(e) {{ + error_obj <- list( + name = class(e)[1], + message = conditionMessage(e), + stack = paste(capture.output(traceback()), collapse = "\n") + ) + write(toJSON(error_obj, auto_unbox = TRUE), "result.json") + stop(e) +}}) +"#, + wm_lib_path = wm_lib_path, + inner_content = inner_content, + spread = spread, + )) +} diff --git a/backend/windmill-worker/src/ruby_executor.rs b/backend/windmill-worker/src/ruby_executor.rs index 82ed16eab0..d94864766b 100644 --- a/backend/windmill-worker/src/ruby_executor.rs +++ b/backend/windmill-worker/src/ruby_executor.rs @@ -29,7 +29,7 @@ use crate::{ get_proxy_envs_for_lang, handle_child::{self}, is_sandboxing_enabled, read_ee_registry_url_list_with_workspace_override, - universal_pkg_installer::{par_install_language_dependencies_seq, RequiredDependency}, + universal_pkg_installer::{par_install_language_dependencies_seq, InstallDeps, RequiredDependency}, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, RUBY_CACHE_DIR, RUBY_REPOS, TRACING_PROXY_CA_CERT_PATH, }; @@ -618,7 +618,7 @@ async fn install<'a>( get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?, ); par_install_language_dependencies_seq( - deps.clone(), + InstallDeps::Flat(deps.clone()), "ruby", "gem", false, @@ -721,7 +721,7 @@ async fn install<'a>( Ok(cmd) }, - // async move |_| Ok(()), + None, &job.id, &job.workspace_id, worker_name, diff --git a/backend/windmill-worker/src/universal_pkg_installer.rs b/backend/windmill-worker/src/universal_pkg_installer.rs index f129371ca1..47b8401184 100644 --- a/backend/windmill-worker/src/universal_pkg_installer.rs +++ b/backend/windmill-worker/src/universal_pkg_installer.rs @@ -1,3 +1,4 @@ +use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; use anyhow::bail; @@ -31,6 +32,153 @@ pub struct RequiredDependency { pub custom_payload: T, } +/// Generic dependency graph that produces topologically sorted layers via Kahn's algorithm. +/// Each layer's packages only depend on packages from earlier layers, enabling parallel install +/// per layer. +#[allow(dead_code)] +pub struct DependencyGraph { + nodes: HashMap>, + deps: HashMap>, +} + +#[allow(dead_code)] +impl DependencyGraph { + pub fn new() -> Self { + Self { nodes: HashMap::new(), deps: HashMap::new() } + } + + /// Insert a dependency and the names of packages it depends on. + /// References to packages not in the graph are silently ignored during layering. + pub fn insert( + &mut self, + key: impl Into, + dep: RequiredDependency, + depends_on: Vec, + ) { + let key = key.into(); + self.nodes.insert(key.clone(), dep); + self.deps.insert(key, depends_on.into_iter().collect()); + } + + /// Render a dependency tree string. Each package appears once, nested under the first parent + /// that pulls it in. Only includes packages present in `filter` (if provided). + pub fn print_tree(&self, filter: Option<&HashSet>) -> String { + // Find roots: packages nothing else in the graph depends on + let mut depended_on: HashSet<&str> = HashSet::new(); + for dep_set in self.deps.values() { + for to in dep_set { + if self.nodes.contains_key(to) { + depended_on.insert(to.as_str()); + } + } + } + let roots: Vec<&String> = self + .nodes + .keys() + .filter(|k| !depended_on.contains(k.as_str())) + .filter(|k| filter.map_or(true, |f| f.contains(*k))) + .sorted() + .collect(); + + let mut out = String::new(); + let mut seen = HashSet::new(); + for root in roots { + self.print_tree_node(root, 0, &mut seen, filter, &mut out); + } + out + } + + fn print_tree_node( + &self, + key: &str, + depth: usize, + seen: &mut HashSet, + filter: Option<&HashSet>, + out: &mut String, + ) { + if !seen.insert(key.to_string()) { + return; + } + if let Some(dep) = self.nodes.get(key) { + let indent = " ".repeat(depth); + out.push_str(&format!("{}- {}\n", indent, dep.display_name)); + if let Some(children) = self.deps.get(key) { + for child in children.iter().sorted() { + if self.nodes.contains_key(child) + && filter.map_or(true, |f| f.contains(child)) + && !seen.contains(child) + { + self.print_tree_node(child, depth + 1, seen, filter, out); + } + } + } + } + } + + /// Produce topologically sorted layers. + pub fn layers(self) -> Vec>> { + let mut in_degree: HashMap = + self.nodes.keys().map(|k| (k.clone(), 0)).collect(); + let mut reverse: HashMap> = HashMap::new(); + + for (from, dep_set) in &self.deps { + for to in dep_set { + if self.nodes.contains_key(to) { + *in_degree.entry(from.clone()).or_default() += 1; + reverse.entry(to.clone()).or_default().push(from.clone()); + } + } + } + + let mut queue: VecDeque = in_degree + .iter() + .filter(|(_, &d)| d == 0) + .map(|(k, _)| k.clone()) + .sorted() + .collect(); + + let mut result = vec![]; + let mut nodes = self.nodes; + + while !queue.is_empty() { + let mut layer = vec![]; + let mut next = VecDeque::new(); + + for key in queue { + if let Some(dep) = nodes.remove(&key) { + layer.push(dep); + } + if let Some(dependents) = reverse.get(&key) { + for d in dependents { + if let Some(deg) = in_degree.get_mut(d) { + *deg -= 1; + if *deg == 0 { + next.push_back(d.clone()); + } + } + } + } + } + + if !layer.is_empty() { + result.push(layer); + } + queue = next.into_iter().sorted().collect(); + } + + result + } +} + +#[allow(dead_code)] +pub enum InstallDeps { + /// Flat list of dependencies — installed in one parallel batch (existing behavior). + Flat(Vec>), + /// Dependency graph — split into topological layers, each installed in parallel. + /// A `--- Layer N ---` separator is printed between layers. + Layered(DependencyGraph), +} + #[allow(dead_code)] pub enum InstallStrategy { /// Will invoke callback to install single dependency @@ -105,8 +253,10 @@ pub async fn par_install_language_dependencies_all_at_once< .await; } let total_time = std::time::Instant::now(); - let (missing, name_max_length) = filter_to_missing(deps, job_id, w_id, jailed, conn).await?; - if missing.is_empty() { + let (layers, name_max_length, total_missing) = + filter_to_missing(InstallDeps::Flat(deps), job_id, w_id, jailed, conn).await?; + let missing: Vec> = layers.into_iter().flatten().collect(); + if total_missing == 0 { return Ok(()); } let to_batch_install = Arc::new(RwLock::new(vec![])); @@ -122,6 +272,9 @@ pub async fn par_install_language_dependencies_all_at_once< conn, _language_name, _platform_agnostic, + None, + None, + None, ) .await?; let installation_res = process_handles(handles, w_id).await; @@ -231,18 +384,26 @@ pub async fn par_install_language_dependencies_seq< 'a, T: Clone + std::marker::Send + Sync + 'a + 'static, >( - deps: Vec>, + install_deps: InstallDeps, _language_name: &'a str, installer_executable_name: &'a str, _platform_agnostic: bool, concurrent_downloads: usize, callback: impl Fn(RequiredDependency) -> Result + Send + Sync + 'static, + post_install: Option) -> anyhow::Result<()> + Send + Sync + 'static>>, job_id: &'a Uuid, w_id: &'a str, worker_name: &'a str, jailed: bool, conn: &'a Connection, ) -> anyhow::Result<()> { + let total_time = std::time::Instant::now(); + let (layers, name_max_length, total_missing) = + filter_to_missing(install_deps, job_id, w_id, jailed, conn).await?; + if total_missing == 0 { + return Ok(()); + } + #[cfg(all(feature = "enterprise", feature = "parquet"))] let is_not_pro = !matches!( windmill_common::ee_oss::get_license_plan().await, @@ -258,65 +419,133 @@ pub async fn par_install_language_dependencies_seq< ) .await; } - let total_time = std::time::Instant::now(); - let (missing, name_max_length) = filter_to_missing(deps, job_id, w_id, jailed, conn).await?; - if missing.is_empty() { - return Ok(()); - } - let handles = spawn_wrapped_installation_threads( - missing, - name_max_length, - InstallStrategy::Single(Arc::new(callback)), - installer_executable_name, - concurrent_downloads, + + let is_layered = layers.len() > 1; + let callback = Arc::new(callback); + let mut offset = 0usize; + + windmill_queue::append_logs( job_id, w_id, - worker_name, + if jailed { + format!( + "\nStarting isolated installation... ({} tasks in parallel)\n", + concurrent_downloads + ) + } else { + format!( + "\nStarting installation... ({} tasks in parallel)\n", + concurrent_downloads + ) + }, conn, - _language_name, - _platform_agnostic, ) - .await?; + .await; + + for (i, layer_deps) in layers.into_iter().enumerate() { + if layer_deps.is_empty() { + continue; + } + + if is_layered && offset > 0 { + windmill_queue::append_logs( + job_id, + w_id, + format!("\n\n--- Layer {} ---", i + 1), + conn, + ) + .await; + } + + let layer_size = layer_deps.len(); + tracing::info!("Layer {}: spawning {} installs", i + 1, layer_size); + let handles = spawn_wrapped_installation_threads( + layer_deps, + name_max_length, + InstallStrategy::Single(callback.clone()), + installer_executable_name, + concurrent_downloads, + job_id, + w_id, + worker_name, + conn, + _language_name, + _platform_agnostic, + Some(offset), + Some(total_missing), + post_install.clone(), + ) + .await?; + tracing::info!("Layer {}: all spawned, waiting for handles", i + 1); + + process_handles(handles, w_id).await?; + tracing::info!("Layer {}: done", i + 1); + offset += layer_size; + } - let installation_res = process_handles(handles, w_id).await; finish_installation(total_time, job_id, w_id, conn).await; - installation_res + Ok(()) } type NameMaxLength = usize; + +/// Returns (layers of missing deps, name_max_length, total_missing). +/// Prints the "To be installed" header once with all missing packages. +/// For `Layered`, prints a dependency tree; for `Flat`, prints a flat list. async fn filter_to_missing<'a, T: Clone + std::marker::Send + Sync + 'a + 'static>( - mut deps: Vec>, + install_deps: InstallDeps, job_id: &Uuid, w_id: &str, jailed: bool, conn: &Connection, -) -> anyhow::Result<(Vec>, NameMaxLength)> { - // Unique to flatten all same values - deps = deps.into_iter().unique_by(|rd| rd.path.clone()).collect(); - // Total to install - let mut missing = vec![]; - // Name max length - let mut name_ml = 0; - for rd in deps.into_iter() { - let display_name = rd.display_name.clone(); - if rd.path.ends_with("/") { - anyhow::bail!("Internal error: path should not end with '/'") +) -> anyhow::Result<(Vec>>, NameMaxLength, usize)> { + let (mut layers, tree_data) = match install_deps { + InstallDeps::Flat(deps) => (vec![deps], None), + InstallDeps::Layered(graph) => { + let deps_map = graph.deps.clone(); + let nodes_display: HashMap = graph + .nodes + .iter() + .map(|(k, v)| (k.clone(), v.display_name.clone())) + .collect(); + let layers = graph.layers(); + (layers, Some((deps_map, nodes_display))) } - { - // Later will help us align text in log console - if display_name.len() > name_ml { + }; + + let mut name_ml = 0; + let mut missing_keys: HashSet = HashSet::new(); + let mut total_missing = 0; + + for layer in layers.iter_mut() { + *layer = std::mem::take(layer) + .into_iter() + .unique_by(|rd| rd.path.clone()) + .collect(); + + let mut missing = vec![]; + for rd in std::mem::take(layer) { + if rd.path.ends_with("/") { + anyhow::bail!("Internal error: path should not end with '/'") + } + if rd.display_name.len() > name_ml { name_ml = rd.display_name.len(); } + if tokio::fs::metadata(rd.path.clone() + ".valid.windmill") + .await + .is_err() + { + if let Some(key) = rd.path.rsplit('/').next() { + missing_keys.insert(key.to_string()); + } + missing.push(rd); + } } - // Will look like: /tmp/windmill/cache/lang/dependency.valid.windmill - if tokio::fs::metadata(rd.path.clone() + ".valid.windmill") - .await - .is_err() - { - missing.push(rd); - } + total_missing += missing.len(); + *layer = missing; } - if !missing.is_empty() { + + if total_missing > 0 { windmill_queue::append_logs( job_id, w_id, @@ -328,15 +557,40 @@ async fn filter_to_missing<'a, T: Clone + std::marker::Send + Sync + 'a + 'stati conn, ) .await; - let to_log = missing - .iter() - .map(|rd| format!("- {}", &rd.display_name)) - .join("\n") - + "\n"; + + let to_log = if let Some((deps_map, nodes_display)) = tree_data { + let mut print_graph: DependencyGraph<()> = DependencyGraph::new(); + for (key, display) in &nodes_display { + if missing_keys.contains(key) { + print_graph.insert( + key.clone(), + RequiredDependency { + path: String::new(), + _s3_handle: String::new(), + display_name: display.clone(), + custom_payload: (), + }, + deps_map + .get(key) + .map(|s| s.iter().cloned().collect()) + .unwrap_or_default(), + ); + } + } + print_graph.print_tree(Some(&missing_keys)) + } else { + layers + .iter() + .flat_map(|l| l.iter()) + .map(|rd| format!("- {}", &rd.display_name)) + .join("\n") + + "\n" + }; windmill_queue::append_logs(job_id, w_id, to_log, conn).await; } - Ok((missing, name_ml)) + + Ok((layers, name_ml, total_missing)) } enum Action { @@ -369,6 +623,9 @@ async fn spawn_wrapped_installation_threads< conn: &Connection, _language_name: &str, _platform_agnostic: bool, + counter_offset: Option, + total_override: Option, + post_install: Option) -> anyhow::Result<()> + Send + Sync + 'static>>, ) -> anyhow::Result<( Vec>>, tokio::sync::broadcast::Sender<()>, @@ -382,11 +639,11 @@ async fn spawn_wrapped_installation_threads< job_id ); - let (mut handles, semaphore, total_to_install, counter_arc) = ( + let total_to_install = total_override.unwrap_or(missing.len()); + let (mut handles, semaphore, counter_arc) = ( vec![], Arc::new(Semaphore::new(parallel_limit)), - missing.len(), - Arc::new(tokio::sync::Mutex::new(0)), + Arc::new(tokio::sync::Mutex::new(counter_offset.unwrap_or(0))), ); // Pretty sensitive. Single drop will fail installation @@ -426,6 +683,7 @@ async fn spawn_wrapped_installation_threads< ), InstallStrategy::AllAtOnce(ref rw_lock) => Action::AddToBulk(Arc::clone(rw_lock)), }; + let post_install_c = post_install.clone(); let task_fut = try_install_one_detached( dep, installer_executable_name.to_owned(), @@ -441,6 +699,7 @@ async fn spawn_wrapped_installation_threads< _platform_agnostic, permit, TaskKiller(kill_tx), + post_install_c, ); handles.push(tokio::spawn(async move { tokio::select! { @@ -513,6 +772,7 @@ async fn try_install_one_detached<'a, T: Clone + std::marker::Send + Sync + 'a + // If dropped the entire installation fails and all installation threads are being stopped // That's why we just pass it to return so it is not being dropped kill_all_tasks: TaskKiller, + post_install: Option) -> anyhow::Result<()> + Send + Sync + 'static>>, ) -> anyhow::Result { let start = std::time::Instant::now(); @@ -607,6 +867,9 @@ async fn try_install_one_detached<'a, T: Clone + std::marker::Send + Sync + 'a + &dep.display_name )); } else { + if let Some(ref cb) = post_install { + cb(&dep)?; + } mark_success(dep.path.clone(), &job_id, &w_id).await; print_success( false, diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 8aeb3a9e5e..e40170dcd3 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -164,6 +164,9 @@ use crate::java_executor::{handle_java_job, JobHandlerInput as JobHandlerInputJa #[cfg(feature = "ruby")] use crate::ruby_executor::{handle_ruby_job, JobHandlerInput as JobHandlerInputRuby}; +#[cfg(feature = "rlang")] +use crate::r_executor::{handle_r_job, JobHandlerInput as JobHandlerInputRlang}; + #[cfg(feature = "php")] use crate::php_executor::handle_php_job; @@ -230,6 +233,9 @@ lazy_static::lazy_static! { // Ruby pub static ref RUBY_CACHE_DIR: String = format!("{}ruby", *ROOT_CACHE_DIR); + // R + pub static ref R_CACHE_DIR: String = format!("{}rlang", *ROOT_CACHE_DIR); + // for related places search: ADD_NEW_LANG pub static ref BUN_CACHE_DIR: String = format!("{}bun", *ROOT_CACHE_NOMOUNT_DIR); pub static ref BUN_BUNDLE_CACHE_DIR: String = format!("{}bun", *ROOT_CACHE_DIR); @@ -4602,7 +4608,8 @@ mount {{ | ScriptLang::Bash | ScriptLang::Powershell | ScriptLang::Ansible - | ScriptLang::Ruby => "#", + | ScriptLang::Ruby + | ScriptLang::Rlang => "#", ScriptLang::Deno | ScriptLang::Bun | ScriptLang::Bunnative @@ -5114,6 +5121,38 @@ mount {{ .await } } + ScriptLang::Rlang => { + #[cfg(not(feature = "rlang"))] + return Err( + anyhow::anyhow!("R is not available because the feature is not enabled").into(), + ); + + #[cfg(feature = "rlang")] + { + if run_inline { + return Err(Error::internal_err( + "Inline execution is not yet supported for this language".to_string(), + )); + } + Box::pin(handle_r_job(JobHandlerInputRlang { + mem_peak, + canceled_by, + job, + conn, + client, + parent_runnable_path, + inner_content: &code, + job_dir, + requirements_o: lock.as_ref(), + shared_mount: &shared_mount, + base_internal_url, + worker_name, + envs, + occupancy_metrics, + })) + .await + } + } // for related places search: ADD_NEW_LANG _ => panic!("unreachable, language is not supported: {language:#?}"), }; @@ -5247,6 +5286,10 @@ pub fn parse_sig_of_lang( ScriptLang::Ruby => Some(windmill_parser_ruby::parse_ruby_signature(code)?), #[cfg(not(feature = "ruby"))] ScriptLang::Ruby => None, + #[cfg(feature = "rlang")] + ScriptLang::Rlang => Some(windmill_parser_r::parse_r_signature(code)?), + #[cfg(not(feature = "rlang"))] + ScriptLang::Rlang => None, // for related places search: ADD_NEW_LANG } } else { diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 5ea5b74d64..dfdd06c2f1 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -61,6 +61,8 @@ use crate::csharp_executor::generate_nuget_lockfile; #[cfg(feature = "java")] use crate::java_executor; +#[cfg(feature = "rlang")] +use crate::r_executor; #[cfg(feature = "ruby")] use crate::ruby_executor; @@ -2763,6 +2765,21 @@ async fn capture_dependency_job( ) .await? } + #[cfg(feature = "rlang")] + ScriptLang::Rlang => { + r_executor::resolve( + job_id, + job_raw_code, + mem_peak, + canceled_by, + job_dir, + &Connection::Sql(db.clone()), + worker_name, + w_id, + false, + ) + .await? + } // for related places search: ADD_NEW_LANG _ => "".to_owned(), }; diff --git a/cli/bootstrap/script_bootstrap.ts b/cli/bootstrap/script_bootstrap.ts index 89d98a1927..43dabca093 100644 --- a/cli/bootstrap/script_bootstrap.ts +++ b/cli/bootstrap/script_bootstrap.ts @@ -134,6 +134,11 @@ public class Main { def main a, b, c puts a, b, c end +`, + rlang: ` +main <- function(x, name = "default") { + return(list(result = x, name = name)) +} `, // for related places search: ADD_NEW_LANG }; diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 1d2b562497..9ab5064658 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -832,6 +832,8 @@ export function filePathExtensionFromContentType( return ".java"; } else if (language === "ruby") { return ".rb"; + } else if (language === "rlang") { + return ".r"; // for related places search: ADD_NEW_LANG } else { throw new Error("Invalid language: " + language); @@ -863,6 +865,7 @@ export const exts = [ ".playbook.yml", ".java", ".rb", + ".r", // for related places search: ADD_NEW_LANG ]; diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index c595af1130..fbf152942a 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -1306,6 +1306,7 @@ export async function elementsToMap( "nu", "java", "rb", + "r", // for related places search: ADD_NEW_LANG ].includes(path.split(".").pop() ?? "") ) { diff --git a/cli/src/guidance/skills.ts b/cli/src/guidance/skills.ts index 053e491122..2417ed9c12 100644 --- a/cli/src/guidance/skills.ts +++ b/cli/src/guidance/skills.ts @@ -24,6 +24,7 @@ export const SKILLS: SkillMetadata[] = [ { name: "write-script-postgresql", description: "MUST use when writing PostgreSQL queries.", languageKey: "postgresql" }, { name: "write-script-powershell", description: "MUST use when writing PowerShell scripts.", languageKey: "powershell" }, { name: "write-script-python3", description: "MUST use when writing Python scripts.", languageKey: "python3" }, + { name: "write-script-rlang", description: "MUST use when writing R scripts.", languageKey: "rlang" }, { name: "write-script-rust", description: "MUST use when writing Rust scripts.", languageKey: "rust" }, { name: "write-script-snowflake", description: "MUST use when writing Snowflake queries.", languageKey: "snowflake" }, { name: "write-flow", description: "MUST use when creating flows." }, @@ -4113,6 +4114,107 @@ async def parallel(items, fn, concurrency: Optional[int] = None) # offset: Message offset to commit (from event['offset']) def commit_kafka_offsets(trigger_path: str, topic: str, partition: int, offset: int) -> None +`, + "write-script-rlang": `--- +name: write-script-rlang +description: MUST use when writing R scripts. +--- + +## CLI Commands + +Place scripts in a folder. After writing, tell the user they can run: +- \`wmill script generate-metadata\` - Generate .script.yaml and .lock files +- \`wmill sync push\` - Deploy to Windmill + +Do NOT run these commands yourself. Instead, inform the user that they should run them. + +Use \`wmill resource-type list --schema\` to discover available resource types. + +# R + +## Structure + +Define a \`main\` function using \`<-\` or \`=\` assignment. Parameters become the script inputs: + +\`\`\`r +library(dplyr) +library(jsonlite) + +main <- function(x, name = "default", flag = TRUE) { + df <- tibble(x = x, name = name) + result <- df %>% mutate(greeting = paste("Hello", name)) + return(toJSON(result, auto_unbox = TRUE)) +} +\`\`\` + +**Important:** +- The \`main\` function is required +- Use \`library()\` to load packages — they are resolved and installed automatically +- \`jsonlite\` is always available (used internally for argument parsing) +- Return values must be JSON-serializable + +## Parameters + +R types map to Windmill types: +- \`numeric\` → float/int +- \`character\` → string +- \`logical\` → bool (use \`TRUE\`/\`FALSE\`) +- \`list\` → object/dict +- \`NULL\` → null + +Default values are inferred from the function signature: + +\`\`\`r +main <- function( + name, # required string + count = 10, # optional int, default 10 + verbose = FALSE # optional bool, default FALSE +) { + # ... +} +\`\`\` + +## Resources and Variables + +Use the built-in Windmill helpers (no import needed): + +\`\`\`r +main <- function() { + # Get a variable + api_key <- get_variable("f/my_folder/api_key") + + # Get a resource (returns a list) + db <- get_resource("f/my_folder/postgres_config") + host <- db$host + port <- db$port + + return(list(host = host, port = port)) +} +\`\`\` + +## Output + +Return any JSON-serializable value from \`main\`. The return value becomes the step result: + +\`\`\`r +main <- function(x) { + # Return a scalar + return(x + 1) + + # Or a list (becomes JSON object) + return(list(result = x + 1, status = "ok")) +} +\`\`\` + +## Annotations + +Control execution behavior with comment annotations: + +\`\`\`r +#renv_verbose = true # Show verbose renv output during resolution +#renv_install_verbose = true # Show verbose output during package installation +#sandbox = true # Run in nsjail sandbox (requires nsjail) +\`\`\` `, "write-script-rust": `--- name: write-script-rust @@ -4359,7 +4461,7 @@ Reference a specific resource using \`$res:\` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, "raw-app": `--- name: raw-app description: MUST use when creating raw apps. diff --git a/cli/src/types.ts b/cli/src/types.ts index 65157ea75c..bfa0a47b9c 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -300,6 +300,7 @@ export function getTypeStrFromPath( parsed.ext == ".nu" || parsed.ext == ".java" || parsed.ext == ".rb" || + parsed.ext == ".r" || // for related places search: ADD_NEW_LANG (parsed.ext == ".yml" && parsed.name.split(".").pop() == "playbook") ) { diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 0e010a0cc5..ccc20f7bcd 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -868,6 +868,9 @@ export async function inferSchema( } else if (language === "ruby") { const { parse_ruby } = await loadParser("windmill-parser-wasm-ruby"); inferedSchema = JSON.parse(parse_ruby(content)); + } else if (language === "rlang") { + const { parse_r } = await loadParser("windmill-parser-wasm-r"); + inferedSchema = JSON.parse(parse_r(content)); // for related places search: ADD_NEW_LANG } else { throw new Error("Invalid language: " + language); diff --git a/cli/src/utils/script_common.ts b/cli/src/utils/script_common.ts index 7f126b6b85..f314fe0e5d 100644 --- a/cli/src/utils/script_common.ts +++ b/cli/src/utils/script_common.ts @@ -20,6 +20,7 @@ export type ScriptLanguage = | "nu" | "ansible" | "ruby" + | "rlang" | "java"; // for related places search: ADD_NEW_LANG @@ -105,6 +106,8 @@ export function inferContentTypeFromFilePath( return "java"; } else if (contentPath.endsWith(".rb")) { return "ruby"; + } else if (contentPath.endsWith(".r")) { + return "rlang"; // for related places search: ADD_NEW_LANG } else { throw new Error( diff --git a/cli/windmill-utils-internal/src/path-utils/path-assigner.ts b/cli/windmill-utils-internal/src/path-utils/path-assigner.ts index a2fc5b8cab..07f423c876 100644 --- a/cli/windmill-utils-internal/src/path-utils/path-assigner.ts +++ b/cli/windmill-utils-internal/src/path-utils/path-assigner.ts @@ -35,6 +35,7 @@ export const LANGUAGE_EXTENSIONS: Record = { duckdb: "duckdb.sql", bunnative: "ts", ruby: "rb", + rlang: "r", // for related places search: ADD_NEW_LANG }; diff --git a/docker/DockerfileFull b/docker/DockerfileFull index 153f4fac5a..91ed372819 100644 --- a/docker/DockerfileFull +++ b/docker/DockerfileFull @@ -27,6 +27,10 @@ RUN /usr/bin/java -jar /usr/bin/coursier about # Ruby RUN apt-get install -y ruby ruby-bundler +# R +RUN apt-get install -y r-base-dev \ + && Rscript -e 'install.packages("renv", lib="/usr/lib/R/library", repos="https://cloud.r-project.org")' + # Fix UV cache permissions for non-root user support (uid 1000, etc.) # The uv tool install ansible command populates the UV cache with root-owned files RUN chmod -R a+rw /tmp/windmill/cache/uv && \ diff --git a/docker/DockerfileFullEe b/docker/DockerfileFullEe index f3bb9c0569..3fbb06da71 100644 --- a/docker/DockerfileFullEe +++ b/docker/DockerfileFullEe @@ -51,6 +51,10 @@ RUN /usr/bin/java -jar /usr/bin/coursier about # Ruby RUN apt-get install -y ruby ruby-bundler +# R +RUN apt-get install -y r-base-dev \ + && Rscript -e 'install.packages("renv", lib="/usr/lib/R/library", repos="https://cloud.r-project.org")' + # iptables RUN apt-get install -y iptables diff --git a/flake.nix b/flake.nix index 5f644d93dc..2bab1c120c 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,16 @@ extensions = [ "rust-src" "rust-analyzer" "rustfmt" ]; }; + patchedClang = pkgs.llvmPackages_18.clang.overrideAttrs (oldAttrs: { + postFixup = '' + # Copy the original postFixup logic but skip add-hardening.sh + ${oldAttrs.postFixup or ""} + + # Remove the line that substitutes add-hardening.sh + sed -i 's/.*source.*add-hardening\.sh.*//' $out/bin/clang + ''; + }); + # --------------------------------------------------------------- # Native C/C++ dependencies (required to compile the backend) # --------------------------------------------------------------- @@ -72,14 +82,16 @@ version = "130.0.7"; target = stdenv.hostPlatform.rust.rustcTarget; sha256 = { - x86_64-linux = "sha256-pkdsuU6bAkcIHEZUJOt5PXdzK424CEgTLXjLtQ80t10="; + x86_64-linux = + "sha256-pkdsuU6bAkcIHEZUJOt5PXdzK424CEgTLXjLtQ80t10="; aarch64-linux = lib.fakeHash; x86_64-darwin = lib.fakeHash; aarch64-darwin = lib.fakeHash; }.${system}; in pkgs.fetchurl { name = "librusty_v8-${version}"; - url = "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_release_${target}.a.gz"; + url = + "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_release_${target}.a.gz"; inherit sha256; }; @@ -87,15 +99,28 @@ # pkg-config search path for native libraries # --------------------------------------------------------------- - pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" - (with pkgs; [ openssl.dev libxml2.dev xmlsec.dev libxslt.dev cyrus_sasl.dev krb5.dev ]); + pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (with pkgs; [ + openssl.dev + libxml2.dev + xmlsec.dev + libxslt.dev + cyrus_sasl.dev + krb5.dev + ]); # --------------------------------------------------------------- # RPATH — embed Nix store library paths into compiled binaries # --------------------------------------------------------------- rpathLibs = lib.makeLibraryPath (with pkgs; [ - openssl libffi cyrus_sasl krb5 libxml2 xmlsec libxslt stdenv.cc.cc.lib + openssl + libffi + cyrus_sasl + krb5 + libxml2 + xmlsec + libxslt + stdenv.cc.cc.lib ]); # --------------------------------------------------------------- @@ -113,11 +138,17 @@ (builtins.readFile "${stdenv.cc}/nix-support/libcxx-cxxflags") "-idirafter ${pkgs.libiconv}/include" ] ++ lib.optionals stdenv.cc.isClang [ - "-idirafter ${stdenv.cc.cc}/lib/clang/${lib.getVersion stdenv.cc.cc}/include" + "-idirafter ${stdenv.cc.cc}/lib/clang/${ + lib.getVersion stdenv.cc.cc + }/include" ] ++ lib.optionals stdenv.cc.isGNU [ "-isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc}" - "-isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc}/${stdenv.hostPlatform.config}" - "-idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${lib.getVersion stdenv.cc.cc}/include" + "-isystem ${stdenv.cc.cc}/include/c++/${ + lib.getVersion stdenv.cc.cc + }/${stdenv.hostPlatform.config}" + "-idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${ + lib.getVersion stdenv.cc.cc + }/include" ]); # --------------------------------------------------------------- @@ -131,12 +162,16 @@ BINDGEN_EXTRA_CLANG_ARGS = bindgenClangArgs; # Force clang 18 as cargo linker (stdenv may bring a newer clang that causes SIGSEGV with mold) - CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "${pkgs.llvmPackages_18.clang}/bin/clang"; - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = "${pkgs.llvmPackages_18.clang}/bin/clang"; + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = + "${pkgs.llvmPackages_18.clang}/bin/clang"; + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = + "${pkgs.llvmPackages_18.clang}/bin/clang"; # Embed rpath so binaries find Nix store .so files at runtime - CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "-C link-arg=-fuse-ld=mold -C link-arg=-Wl,-rpath,${rpathLibs}"; - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "-C link-arg=-fuse-ld=mold -C link-arg=-Wl,-rpath,${rpathLibs}"; + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS = + "-C link-arg=-fuse-ld=mold -C link-arg=-Wl,-rpath,${rpathLibs}"; + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS = + "-C link-arg=-fuse-ld=mold -C link-arg=-Wl,-rpath,${rpathLibs}"; CARGO_HOST_RUSTFLAGS = "-C link-arg=-Wl,-rpath,${rpathLibs}"; # https://github.com/NixOS/nixpkgs/issues/370494 — jemalloc build fix @@ -197,6 +232,10 @@ hash = "sha256-8E0WtDFc7RcqmftDigMyy1xXUkjgL4X4kpf7h1GdE48="; }; + rWithPackages = pkgs.rWrapper.override { + packages = with pkgs.rPackages; [ renv ]; + }; + extraRuntimes = with pkgs; [ dotnet-sdk_9 php @@ -222,6 +261,7 @@ ANSIBLE_PLAYBOOK_PATH = "${pkgs.ansible}/bin/ansible-playbook"; ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy"; CARGO_SWEEP_PATH = "${pkgs.cargo-sweep}/bin/cargo-sweep"; + RSCRIPT_PATH = "${rWithPackages}/bin/Rscript"; }; # --------------------------------------------------------------- @@ -251,13 +291,23 @@ (pkgs.writeScriptBin "wm" '' cd ./frontend npm install - npm run ${if stdenv.isDarwin then "generate-backend-client-mac" else "generate-backend-client"} + npm run ${ + if stdenv.isDarwin then + "generate-backend-client-mac" + else + "generate-backend-client" + } npm run dev "$@" '') (pkgs.writeScriptBin "wm-build" '' cd ./frontend npm install - npm run ${if stdenv.isDarwin then "generate-backend-client-mac" else "generate-backend-client"} + npm run ${ + if stdenv.isDarwin then + "generate-backend-client-mac" + else + "generate-backend-client" + } npm run build "$@" '') (pkgs.writeScriptBin "wm-migrate" '' @@ -322,22 +372,20 @@ # Shared inputs and settings for default + full shells # --------------------------------------------------------------- - coreBuildInputs = nativeBuildDeps ++ commonRuntimes ++ [ - rustStable - openapi-generator-cli - ] ++ (with pkgs; [ - nodejs - git - sqlx-cli - cargo-watch - jq - gnused + coreBuildInputs = nativeBuildDeps ++ commonRuntimes + ++ [ rustStable openapi-generator-cli ] ++ (with pkgs; [ + nodejs + git + sqlx-cli + cargo-watch + jq + gnused - # CLI tools (for AI agents and dev workflow) - gh - asciinema - mermaid-cli - ]); + # CLI tools (for AI agents and dev workflow) + gh + asciinema + mermaid-cli + ]); # Playwright: use Nix-provided browsers (version-matched to playwright-driver) # Mermaid/Puppeteer: point at Nix chromium (Puppeteer respects this env var) @@ -380,16 +428,26 @@ sandboxEnv = pkgs.buildEnv { name = "windmill-sandbox"; - paths = coreBuildInputs ++ helperScriptsBase - ++ [ playwrightWrapper sandboxEnvScript pkgConfigWrapper pkgs.chromium ]; + paths = coreBuildInputs ++ helperScriptsBase ++ [ + playwrightWrapper + sandboxEnvScript + pkgConfigWrapper + pkgs.chromium + ]; }; sandboxFullEnv = pkgs.buildEnv { name = "windmill-sandbox-full"; - paths = coreBuildInputs ++ extraRuntimes - ++ helperScriptsBase ++ helperScriptsFull - ++ [ playwrightWrapper sandboxEnvScript pkgConfigWrapper pkgs.chromium - pkgs.cargo-sweep pkgs.xcaddy pkgs.nsjail ]; + paths = coreBuildInputs ++ extraRuntimes ++ helperScriptsBase + ++ helperScriptsFull ++ [ + playwrightWrapper + sandboxEnvScript + pkgConfigWrapper + pkgs.chromium + pkgs.cargo-sweep + pkgs.xcaddy + pkgs.nsjail + ]; }; in { @@ -412,8 +470,8 @@ shellHook = devShellHook; buildInputs = coreBuildInputs; - packages = helperScriptsBase ++ [ playwrightWrapper ]; - }); + packages = helperScriptsBase ++ [ playwrightWrapper ]; + }); # ============================================================= # full — all language runtimes, k8s tooling, specialized scripts @@ -428,27 +486,28 @@ pyright openapi-python-client - # LSP / editor - svelte-language-server - taplo + # LSP / editor + svelte-language-server + taplo - # Extra dev tools - cargo-sweep + # Extra dev tools + cargo-sweep - # Kubernetes - minikube - kubectl - kubernetes-helm - conntrack-tools - cri-tools + # Kubernetes + minikube + kubectl + kubernetes-helm + conntrack-tools + cri-tools - # Extra - xcaddy - nsjail - ]); + # Extra + xcaddy + nsjail + ]); - packages = helperScriptsBase ++ helperScriptsFull ++ [ playwrightWrapper ]; - }); + packages = helperScriptsBase ++ helperScriptsFull + ++ [ playwrightWrapper ]; + }); # ============================================================= # wasm — WASM target compilation (nightly Rust) @@ -458,15 +517,23 @@ devShells.wasm = pkgs.mkShell (buildEnvVars // { hardeningDisable = [ "all" ]; + # Explicitly set paths for headers and linker + # DO NOT REMOVE - if absent, breaks wasm builds on NixOS. + shellHook = '' + export CC=${patchedClang}/bin/clang + ''; + buildInputs = nativeBuildDeps ++ (with pkgs; [ (rust-bin.nightly.latest.default.override { extensions = [ "rust-src" "rust-analyzer" ]; - targets = [ "wasm32-unknown-unknown" "wasm32-unknown-emscripten" ]; + targets = + [ "wasm32-unknown-unknown" "wasm32-unknown-emscripten" ]; }) wasm-pack deno emscripten nushell + nodejs glibc_multi ]); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5a6ba807ca..b51ac6cb73 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -83,6 +83,7 @@ "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.647.1", "windmill-parser-wasm-py": "1.657.2", + "windmill-parser-wasm-r": "^1.668.1", "windmill-parser-wasm-regex": "1.653.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", @@ -843,7 +844,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -855,7 +855,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -866,7 +865,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1356,7 +1354,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1513,7 +1510,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1530,7 +1526,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1547,7 +1542,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1564,7 +1558,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1581,7 +1574,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1598,7 +1590,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1615,7 +1606,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1632,7 +1622,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1649,7 +1638,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1666,7 +1654,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1683,7 +1670,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1700,7 +1686,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1717,7 +1702,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1734,7 +1718,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1751,7 +1734,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2057,7 +2039,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6866,7 +6847,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==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7365,7 +7346,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7386,7 +7366,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7407,7 +7386,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7428,7 +7406,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7449,7 +7426,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7470,7 +7446,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7491,7 +7466,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7512,7 +7486,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7533,7 +7506,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7554,7 +7526,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7575,7 +7546,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12151,21 +12121,6 @@ } } }, - "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", @@ -12896,7 +12851,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13685,6 +13640,11 @@ "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.657.2.tgz", "integrity": "sha512-3CN2rziafgCWcZri812+CkzuaE3P3/7dXmV9lSDpK9ma6Esd4zkHRXUFSyRzQE/R7Fxj5mSmSNX6xTff8eX5mw==" }, + "node_modules/windmill-parser-wasm-r": { + "version": "1.668.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-r/-/windmill-parser-wasm-r-1.668.1.tgz", + "integrity": "sha512-5YNeUibxpNBvYrxCgQcz1PxGhTFx2CyEpg2udtIhq7bx0d4gF/KDZVupMeQmAObmrEtTSFGUWNRJ4zXSWNrSpQ==" + }, "node_modules/windmill-parser-wasm-regex": { "version": "1.653.0", "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.653.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index c876f4f26e..99878f23d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -156,6 +156,7 @@ "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.647.1", "windmill-parser-wasm-py": "1.657.2", + "windmill-parser-wasm-r": "1.668.1", "windmill-parser-wasm-regex": "1.653.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 9e7f2eb81e..89f8b803a2 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -156,6 +156,7 @@ 'nu', 'java', 'ruby', + 'rlang', 'postgresql', 'mysql', 'bigquery', @@ -182,7 +183,8 @@ 'csharp', 'nu', 'java', - 'ruby' + 'ruby', + 'rlang' // for related places search: ADD_NEW_LANG ].includes(lang ?? '') ) @@ -202,7 +204,8 @@ 'csharp', 'nu', 'java', - 'ruby' + 'ruby', + 'rlang' // for related places search: ADD_NEW_LANG ].includes(lang ?? '') ) @@ -515,6 +518,8 @@ // for related places search: ADD_NEW_LANG } else if (lang == 'ruby') { editor.insertAtCursor(`ENV['${name}']`) + } else if (lang == 'rlang') { + editor.insertAtCursor(`Sys.getenv("${name}")`) } else if ( ['postgresql', 'mysql', 'bigquery', 'mssql', 'oracledb', 'snowflake', 'duckdb'].includes( lang ?? '' @@ -583,6 +588,8 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri); editor.insertAtBeginning("require 'windmill/mini'\n") } editor.insertAtCursor(`get_variable("${path}")`) + } else if (lang == 'rlang') { + editor.insertAtCursor(`get_variable("${path}")`) } sendUserToast(`${name} inserted at cursor`) }} @@ -662,6 +669,8 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS editor.insertAtBeginning("require 'windmill/mini'\n") } editor.insertAtCursor(`get_resource("${path}")`) + } else if (lang == 'rlang') { + editor.insertAtCursor(`get_resource("${path}")`) } else if (lang == 'duckdb') { let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType] if (!t) { diff --git a/frontend/src/lib/components/HighlightCode.svelte b/frontend/src/lib/components/HighlightCode.svelte index be6e6b6ef0..909b8daca2 100644 --- a/frontend/src/lib/components/HighlightCode.svelte +++ b/frontend/src/lib/components/HighlightCode.svelte @@ -14,6 +14,7 @@ import yaml from 'svelte-highlight/languages/yaml' import java from 'svelte-highlight/languages/java' import ruby from 'svelte-highlight/languages/ruby' + import r from 'svelte-highlight/languages/r' import type { Script } from '$lib/gen' import { Button } from './common' import { copyToClipboard } from '$lib/utils' @@ -91,6 +92,8 @@ return java case 'ruby': return ruby + case 'rlang': + return r case 'json': return json // for related places search: ADD_NEW_LANG diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 16ebfa8851..d55fe12146 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -1268,7 +1268,7 @@ } as ButtonType.Icon} > {label} - {#if lang === 'ruby'} + {#if lang === 'rlang'} BETA {/if} diff --git a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte index ca3900eb9f..09ff1082e6 100644 --- a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte +++ b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte @@ -25,6 +25,7 @@ import JavaIcon from '$lib/components/icons/JavaIcon.svelte' import DuckDbIcon from '$lib/components/icons/DuckDbIcon.svelte' import RubyIcon from '$lib/components/icons/RubyIcon.svelte' + import RIcon from '$lib/components/icons/RIcon.svelte' import ClaudeIcon from '$lib/components/icons/ClaudeIcon.svelte' interface Props { @@ -72,6 +73,7 @@ nu: 'Nu', java: 'Java', ruby: 'Ruby', + rlang: 'R', claudesandbox: 'Claude Sandbox' // for related places search: ADD_NEW_LANG } @@ -107,6 +109,7 @@ nu: NuIcon, java: JavaIcon, ruby: RubyIcon, + rlang: RIcon, duckdb: DuckDbIcon, claudesandbox: TypeScriptIcon // for related places search: ADD_NEW_LANG diff --git a/frontend/src/lib/components/icons/RIcon.svelte b/frontend/src/lib/components/icons/RIcon.svelte new file mode 100644 index 0000000000..388193fc2d --- /dev/null +++ b/frontend/src/lib/components/icons/RIcon.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/src/lib/components/worker_group.ts b/frontend/src/lib/components/worker_group.ts index 0547c811c9..d0391b17c2 100644 --- a/frontend/src/lib/components/worker_group.ts +++ b/frontend/src/lib/components/worker_group.ts @@ -58,6 +58,7 @@ export const defaultTags = [ 'nu', 'java', 'ruby', + 'rlang', 'duckdb' // for related places search: ADD_NEW_LANG ] diff --git a/frontend/src/lib/editorLangUtils.ts b/frontend/src/lib/editorLangUtils.ts index c52ff51f74..3152eff946 100644 --- a/frontend/src/lib/editorLangUtils.ts +++ b/frontend/src/lib/editorLangUtils.ts @@ -99,6 +99,8 @@ export function extToLang(ext: string) { return 'java' case 'rb': return 'ruby' + case 'r': + return 'r' // for related places search: ADD_NEW_LANG default: return 'unknown' diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index 4624b777fa..fcc1d2f7c8 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -42,6 +42,7 @@ import initCSharpParser, { parse_csharp } from 'windmill-parser-wasm-csharp' import initNuParser, { parse_nu } from 'windmill-parser-wasm-nu' import initJavaParser, { parse_java } from 'windmill-parser-wasm-java' import initRubyParser, { parse_ruby } from 'windmill-parser-wasm-ruby' +import initRParser, { parse_r } from 'windmill-parser-wasm-r' import wasmUrlTs from 'windmill-parser-wasm-ts/windmill_parser_wasm_bg.wasm?url' import wasmUrlRegex from 'windmill-parser-wasm-regex/windmill_parser_wasm_bg.wasm?url' @@ -54,6 +55,7 @@ import wasmUrlCSharp from 'windmill-parser-wasm-csharp/windmill_parser_wasm_bg.w import wasmUrlNu from 'windmill-parser-wasm-nu/windmill_parser_wasm_bg.wasm?url' import wasmUrlJava from 'windmill-parser-wasm-java/windmill_parser_wasm_bg.wasm?url' import wasmUrlRuby from 'windmill-parser-wasm-ruby/windmill_parser_wasm_bg.wasm?url' +import wasmUrlR from 'windmill-parser-wasm-r/windmill_parser_wasm_bg.wasm?url' import wasmUrlAsset from 'windmill-parser-wasm-asset/windmill_parser_wasm_bg.wasm?url' import initWacParser, { parse_workflow_as_code } from 'windmill-parser-wasm-wac' import wasmUrlWac from 'windmill-parser-wasm-wac/windmill_parser_wasm_bg.wasm?url' @@ -101,6 +103,9 @@ async function initWasmJava() { async function initWasmRuby() { await initRubyParser(wasmUrlRuby) } +async function initWasmR() { + await initRParser(wasmUrlR) +} async function initWasmAsset() { await initAssetParser(wasmUrlAsset) } @@ -211,6 +216,7 @@ function getCommentPrefix(language: SupportedLanguage | undefined): string | und case 'powershell': case 'ansible': case 'ruby': + case 'rlang': return '#' case 'deno': case 'bun': @@ -418,6 +424,13 @@ export async function inferArgs( } else if (language == 'ruby') { await initWasmRuby() inferedSchema = JSON.parse(parse_ruby(code)) + } else if (language == 'rlang') { + try { + await initWasmR() + inferedSchema = JSON.parse(parse_r(code)) + } catch { + inferedSchema = parseRSignatureFallback(code) + } // for related places search: ADD_NEW_LANG } else { return null @@ -550,3 +563,76 @@ export async function parseOutputs( } return outputs.error ? [] : outputs.outputs } + +/** JS fallback parser for R main() signatures when WASM parser is unavailable. */ +function parseRSignatureFallback(code: string): MainArgSignature { + const result: MainArgSignature = { + type: 'Valid', + error: '', + star_args: false, + star_kwargs: false, + args: [], + has_preprocessor: null, + auto_kind: null + } + + const mainMatch = code.match(/\bmain\s*(?:<-|=)\s*function\s*\(([^)]*)\)/) + if (!mainMatch) { + return result + } + + const paramsStr = mainMatch[1].trim() + if (!paramsStr) return result + + // Split params respecting nested parens + const params: string[] = [] + let depth = 0 + let current = '' + for (const ch of paramsStr) { + if ('([{'.includes(ch)) { + depth++ + current += ch + } else if (')]}'.includes(ch)) { + depth-- + current += ch + } else if (ch === ',' && depth === 0) { + params.push(current) + current = '' + } else { + current += ch + } + } + if (current.trim()) params.push(current) + + for (const param of params) { + const trimmed = param.trim() + if (!trimmed) continue + + const eqIndex = trimmed.indexOf('=') + if (eqIndex === -1) { + result.args.push({ name: trimmed, typ: 'unknown', has_default: false, default: undefined }) + } else { + const name = trimmed.slice(0, eqIndex).trim() + const raw = trimmed.slice(eqIndex + 1).trim() + const parsed = parseRDefault(raw) + result.args.push({ name, typ: parsed.typ, has_default: true, default: parsed.value }) + } + } + + return result +} + +function parseRDefault(raw: string): { value: unknown; typ: MainArgSignature['args'][0]['typ'] } { + if (raw === 'TRUE' || raw === 'true') return { value: true, typ: 'bool' } + if (raw === 'FALSE' || raw === 'false') return { value: false, typ: 'bool' } + if (raw === 'NULL') return { value: null, typ: 'unknown' } + if (/^-?\d+(\.\d+)?$/.test(raw)) { + const num = Number(raw) + if (Number.isInteger(num) && !raw.includes('.')) return { value: num, typ: 'int' } + return { value: num, typ: 'float' } + } + const strMatch = raw.match(/^"((?:[^"\\]|\\.)*)"$/) || raw.match(/^'((?:[^'\\]|\\.)*)'$/) + if (strMatch) return { value: strMatch[1], typ: { str: null } } + if (raw.startsWith('list(') || raw.startsWith('c(')) return { value: null, typ: { list: null } } + return { value: null, typ: 'unknown' } +} diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index 6865637ba7..a836debbdd 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -1283,6 +1283,26 @@ def main( return result end ` +const R_INIT_CODE = `library(dplyr) +library(jsonlite) + +main <- function( + x, + name = "default", + age = 25, + data = list(1, 2, 3), + flag = TRUE +) { + # Use Windmill helpers: + # var <- get_variable("f/my_var") + # res <- get_resource("f/my_resource") + + df <- tibble(name = name, age = age, x = x) + result <- df %>% mutate(greeting = paste("Hello", name)) + + return(toJSON(result, auto_unbox = TRUE)) +} +` // for related places search: ADD_NEW_LANG export const INITIAL_CODE = { bun: { @@ -1378,6 +1398,9 @@ export const INITIAL_CODE = { ruby: { script: RUBY_INIT_CODE }, + rlang: { + script: R_INIT_CODE + }, claudesandbox: { script: CLAUDE_SANDBOX_INIT_CODE }, @@ -1507,6 +1530,8 @@ export function initialCode( return INITIAL_CODE.java.script } else if (language == 'ruby') { return INITIAL_CODE.ruby.script + } else if (language == 'rlang') { + return INITIAL_CODE.rlang.script // for related places search: ADD_NEW_LANG } else if (language == 'bun' || language == 'bunnative') { if (subkind === 'claudesandbox') { diff --git a/frontend/src/lib/scripts.ts b/frontend/src/lib/scripts.ts index 5ddeaeb148..f1518f4368 100644 --- a/frontend/src/lib/scripts.ts +++ b/frontend/src/lib/scripts.ts @@ -61,6 +61,8 @@ export function scriptLangToEditorLang( return 'nu' } else if (lang == 'java') { return 'java' + } else if (lang == 'rlang') { + return 'r' // for related places search: ADD_NEW_LANG } else if (lang == undefined) { return 'typescript' @@ -163,7 +165,8 @@ const scriptLanguagesArray: [SupportedLanguage | 'docker' | 'bunnative', string] ['nu', 'Nu'], ['java', 'Java'], ['duckdb', 'DuckDB'], - ['ruby', 'Ruby'] + ['ruby', 'Ruby'], + ['rlang', 'R'] // for related places search: ADD_NEW_LANG ] export function processLangs(selected: string | undefined, langs: string[]): string[] { @@ -173,7 +176,7 @@ export function processLangs(selected: string | undefined, langs: string[]): str let ls = langs.filter((lang) => lang !== 'nativets') //those languages are newer and may not be in the saved list - let nl = ['bunnative', 'rust', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'ruby'] + let nl = ['bunnative', 'rust', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'ruby', 'rlang'] // for related places search: ADD_NEW_LANG nl.forEach((lang) => { if (!ls.includes(lang)) { diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 8889703621..8201e0aea2 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -661,6 +661,7 @@ components: - nu - java - ruby + - rlang - duckdb # for related places search: ADD_NEW_LANG path: diff --git a/system_prompts/auto-generated/flow.md b/system_prompts/auto-generated/flow.md index 695008cbae..92c8ef0c27 100644 --- a/system_prompts/auto-generated/flow.md +++ b/system_prompts/auto-generated/flow.md @@ -120,4 +120,4 @@ Reference a specific resource using `$res:` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 747dc8a7fe..9203f7fa84 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -1523,7 +1523,7 @@ class SqlQuery: export const OPENFLOW_SCHEMA = `## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`; +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`; export const CLI_COMMANDS = `# Windmill CLI Commands @@ -3134,6 +3134,93 @@ result: S3Object = wmill.write_s3_file( \`\`\` `; +export const LANG_RLANG = `# R + +## Structure + +Define a \`main\` function using \`<-\` or \`=\` assignment. Parameters become the script inputs: + +\`\`\`r +library(dplyr) +library(jsonlite) + +main <- function(x, name = "default", flag = TRUE) { + df <- tibble(x = x, name = name) + result <- df %>% mutate(greeting = paste("Hello", name)) + return(toJSON(result, auto_unbox = TRUE)) +} +\`\`\` + +**Important:** +- The \`main\` function is required +- Use \`library()\` to load packages — they are resolved and installed automatically +- \`jsonlite\` is always available (used internally for argument parsing) +- Return values must be JSON-serializable + +## Parameters + +R types map to Windmill types: +- \`numeric\` → float/int +- \`character\` → string +- \`logical\` → bool (use \`TRUE\`/\`FALSE\`) +- \`list\` → object/dict +- \`NULL\` → null + +Default values are inferred from the function signature: + +\`\`\`r +main <- function( + name, # required string + count = 10, # optional int, default 10 + verbose = FALSE # optional bool, default FALSE +) { + # ... +} +\`\`\` + +## Resources and Variables + +Use the built-in Windmill helpers (no import needed): + +\`\`\`r +main <- function() { + # Get a variable + api_key <- get_variable("f/my_folder/api_key") + + # Get a resource (returns a list) + db <- get_resource("f/my_folder/postgres_config") + host <- db$host + port <- db$port + + return(list(host = host, port = port)) +} +\`\`\` + +## Output + +Return any JSON-serializable value from \`main\`. The return value becomes the step result: + +\`\`\`r +main <- function(x) { + # Return a scalar + return(x + 1) + + # Or a list (becomes JSON object) + return(list(result = x + 1, status = "ok")) +} +\`\`\` + +## Annotations + +Control execution behavior with comment annotations: + +\`\`\`r +#renv_verbose = true # Show verbose renv output during resolution +#renv_install_verbose = true # Show verbose output during package installation +#sandbox = true # Run in nsjail sandbox (requires nsjail) +\`\`\` +`; + export const LANG_RUST = `# Rust ## Structure diff --git a/system_prompts/auto-generated/script.md b/system_prompts/auto-generated/script.md index 674c9986b9..bd9eaf1764 100644 --- a/system_prompts/auto-generated/script.md +++ b/system_prompts/auto-generated/script.md @@ -1032,6 +1032,93 @@ result: S3Object = wmill.write_s3_file( ``` +# R + +## Structure + +Define a `main` function using `<-` or `=` assignment. Parameters become the script inputs: + +```r +library(dplyr) +library(jsonlite) + +main <- function(x, name = "default", flag = TRUE) { + df <- tibble(x = x, name = name) + result <- df %>% mutate(greeting = paste("Hello", name)) + return(toJSON(result, auto_unbox = TRUE)) +} +``` + +**Important:** +- The `main` function is required +- Use `library()` to load packages — they are resolved and installed automatically +- `jsonlite` is always available (used internally for argument parsing) +- Return values must be JSON-serializable + +## Parameters + +R types map to Windmill types: +- `numeric` → float/int +- `character` → string +- `logical` → bool (use `TRUE`/`FALSE`) +- `list` → object/dict +- `NULL` → null + +Default values are inferred from the function signature: + +```r +main <- function( + name, # required string + count = 10, # optional int, default 10 + verbose = FALSE # optional bool, default FALSE +) { + # ... +} +``` + +## Resources and Variables + +Use the built-in Windmill helpers (no import needed): + +```r +main <- function() { + # Get a variable + api_key <- get_variable("f/my_folder/api_key") + + # Get a resource (returns a list) + db <- get_resource("f/my_folder/postgres_config") + host <- db$host + port <- db$port + + return(list(host = host, port = port)) +} +``` + +## Output + +Return any JSON-serializable value from `main`. The return value becomes the step result: + +```r +main <- function(x) { + # Return a scalar + return(x + 1) + + # Or a list (becomes JSON object) + return(list(result = x + 1, status = "ok")) +} +``` + +## Annotations + +Control execution behavior with comment annotations: + +```r +#renv_verbose = true # Show verbose renv output during resolution +#renv_install_verbose = true # Show verbose output during package installation +#sandbox = true # Run in nsjail sandbox (requires nsjail) +``` + + # Rust ## Structure diff --git a/system_prompts/auto-generated/skills/write-flow/SKILL.md b/system_prompts/auto-generated/skills/write-flow/SKILL.md index 6f7dec8234..387401a557 100644 --- a/system_prompts/auto-generated/skills/write-flow/SKILL.md +++ b/system_prompts/auto-generated/skills/write-flow/SKILL.md @@ -125,4 +125,4 @@ Reference a specific resource using `$res:` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file diff --git a/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md b/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md new file mode 100644 index 0000000000..0404c0fe5c --- /dev/null +++ b/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md @@ -0,0 +1,100 @@ +--- +name: write-script-rlang +description: MUST use when writing R scripts. +--- + +## CLI Commands + +Place scripts in a folder. After writing, tell the user they can run: +- `wmill script generate-metadata` - Generate .script.yaml and .lock files +- `wmill sync push` - Deploy to Windmill + +Do NOT run these commands yourself. Instead, inform the user that they should run them. + +Use `wmill resource-type list --schema` to discover available resource types. + +# R + +## Structure + +Define a `main` function using `<-` or `=` assignment. Parameters become the script inputs: + +```r +library(dplyr) +library(jsonlite) + +main <- function(x, name = "default", flag = TRUE) { + df <- tibble(x = x, name = name) + result <- df %>% mutate(greeting = paste("Hello", name)) + return(toJSON(result, auto_unbox = TRUE)) +} +``` + +**Important:** +- The `main` function is required +- Use `library()` to load packages — they are resolved and installed automatically +- `jsonlite` is always available (used internally for argument parsing) +- Return values must be JSON-serializable + +## Parameters + +R types map to Windmill types: +- `numeric` → float/int +- `character` → string +- `logical` → bool (use `TRUE`/`FALSE`) +- `list` → object/dict +- `NULL` → null + +Default values are inferred from the function signature: + +```r +main <- function( + name, # required string + count = 10, # optional int, default 10 + verbose = FALSE # optional bool, default FALSE +) { + # ... +} +``` + +## Resources and Variables + +Use the built-in Windmill helpers (no import needed): + +```r +main <- function() { + # Get a variable + api_key <- get_variable("f/my_folder/api_key") + + # Get a resource (returns a list) + db <- get_resource("f/my_folder/postgres_config") + host <- db$host + port <- db$port + + return(list(host = host, port = port)) +} +``` + +## Output + +Return any JSON-serializable value from `main`. The return value becomes the step result: + +```r +main <- function(x) { + # Return a scalar + return(x + 1) + + # Or a list (becomes JSON object) + return(list(result = x + 1, status = "ok")) +} +``` + +## Annotations + +Control execution behavior with comment annotations: + +```r +#renv_verbose = true # Show verbose renv output during resolution +#renv_install_verbose = true # Show verbose output during package installation +#sandbox = true # Run in nsjail sandbox (requires nsjail) +``` diff --git a/system_prompts/languages/rlang.md b/system_prompts/languages/rlang.md new file mode 100644 index 0000000000..7ae3b34525 --- /dev/null +++ b/system_prompts/languages/rlang.md @@ -0,0 +1,85 @@ +# R + +## Structure + +Define a `main` function using `<-` or `=` assignment. Parameters become the script inputs: + +```r +library(dplyr) +library(jsonlite) + +main <- function(x, name = "default", flag = TRUE) { + df <- tibble(x = x, name = name) + result <- df %>% mutate(greeting = paste("Hello", name)) + return(toJSON(result, auto_unbox = TRUE)) +} +``` + +**Important:** +- The `main` function is required +- Use `library()` to load packages — they are resolved and installed automatically +- `jsonlite` is always available (used internally for argument parsing) +- Return values must be JSON-serializable + +## Parameters + +R types map to Windmill types: +- `numeric` → float/int +- `character` → string +- `logical` → bool (use `TRUE`/`FALSE`) +- `list` → object/dict +- `NULL` → null + +Default values are inferred from the function signature: + +```r +main <- function( + name, # required string + count = 10, # optional int, default 10 + verbose = FALSE # optional bool, default FALSE +) { + # ... +} +``` + +## Resources and Variables + +Use the built-in Windmill helpers (no import needed): + +```r +main <- function() { + # Get a variable + api_key <- get_variable("f/my_folder/api_key") + + # Get a resource (returns a list) + db <- get_resource("f/my_folder/postgres_config") + host <- db$host + port <- db$port + + return(list(host = host, port = port)) +} +``` + +## Output + +Return any JSON-serializable value from `main`. The return value becomes the step result: + +```r +main <- function(x) { + # Return a scalar + return(x + 1) + + # Or a list (becomes JSON object) + return(list(result = x + 1, status = "ok")) +} +``` + +## Annotations + +Control execution behavior with comment annotations: + +```r +#renv_verbose = true # Show verbose renv output during resolution +#renv_install_verbose = true # Show verbose output during package installation +#sandbox = true # Run in nsjail sandbox (requires nsjail) +``` diff --git a/system_prompts/utils.py b/system_prompts/utils.py index ee77e20c13..4318b81642 100644 --- a/system_prompts/utils.py +++ b/system_prompts/utils.py @@ -177,6 +177,11 @@ LANGUAGE_METADATA = { 'description': 'MUST use when writing Java scripts.', 'use_cases': 'Java automation, enterprise integrations' }, + 'rlang': { + 'name': 'R', + 'description': 'MUST use when writing R scripts.', + 'use_cases': 'R statistical computing, data analysis, visualization' + }, } # Languages that use TypeScript SDK