Merge remote-tracking branch 'origin/main' into glm/add-local-dev-proxy-v2

# Conflicts:
#	cli/src/commands/dev/dev.ts
#	cli/src/commands/sync/sync.ts
#	cli/src/guidance/writer.ts
This commit is contained in:
Guilhem Lemouel
2026-04-24 10:26:41 +02:00
195 changed files with 9447 additions and 2564 deletions
+265
View File
@@ -0,0 +1,265 @@
---
name: adding-a-trigger
description: Checklist for adding a new TriggerCrud-based trigger type to Windmill (Azure, GCP, Kafka, etc.). Use when wiring a new trigger kind across backend, frontend, CLI, and capture infrastructure.
---
# Skill: Adding a New Trigger Type
Use this skill when adding a trigger kind that implements `TriggerCrud` (Kafka, GCP, Azure, MQTT, SQS, NATS, Postgres, Email…). For native triggers (Nextcloud, Google Drive — things wired through `windmill-native-triggers`), use the `native-trigger` skill instead.
The goal of this doc is to enumerate every file that needs to change. Missing any one of them leads to silent regressions: sync drops the trigger, capture button does nothing, workspace forks lose it, sidebar counters undercount. Follow the checklist top-to-bottom — each section is independent enough to be validated on its own.
Throughout this doc, substitute `{kind}` for the new trigger kind (`azure`, `kafka`, …), `{Kind}` for PascalCase (`Azure`, `Kafka`), `{KIND}` for SCREAMING (`AZURE`, `KAFKA`).
## Reference implementations
- **GCP** — closest analogue to Azure. Has push + pull, OIDC auth, ARM-like resource paths, capture handler. Grep for `gcp_trigger` / `GcpTrigger`.
- **Kafka** — simpler (pull-only, streaming). Good for trivial integrations.
- **Azure** — most recently added (2026). Shared-secret push auth, Event Grid namespaces + basic topics, ARM resource discovery, Namespace-pull data-plane. Grep for `azure_trigger` / `AzureTrigger`.
## 1. Database migration
Create a migration: `cargo sqlx migrate add -r add_{kind}_trigger` from `backend/`. Never write timestamps manually.
The `up.sql` usually defines:
- An optional enum type (e.g. `AZURE_MODE`) if the trigger has sub-kinds
- The `{kind}_trigger` table with at minimum these columns (mirrored from kafka/gcp):
- primary: `(workspace_id, path)`
- `script_path`, `is_flow`, `enabled`, `mode`, `permissioned_as`, `edited_by`, `email`
- `edited_at`, `error`, `server_id`, `last_server_ping`
- `error_handler_path`, `error_handler_args jsonb`, `retry jsonb`
- trigger-specific fields
- Indexes on foreign keys + any frequently-filtered columns
- Foreign key to `workspace`
Down migration drops the table and any enum types.
## 2. Backend crate (`windmill-trigger-{kind}`)
Create a new crate under `backend/windmill-trigger-{kind}/` with:
- `Cargo.toml`: features `enterprise`, `private` if EE, standard deps
- `src/lib.rs`: `pub use mod_ee::*;` behind `#[cfg(all(feature = "enterprise", feature = "private"))]`
- `src/mod_ee.rs`: core types + helpers
- `src/handler_ee.rs`: `TriggerCrud` impl + route handlers
- `src/listener_ee.rs`: (only if streaming/pull-based) `Listener` trait impl
Required in `mod_ee.rs`:
- `{Kind}Config` struct (persisted shape, `FromRow`)
- `{Kind}ConfigRequest` struct (what API receives — usually similar to Config but with validation fields)
- `{Kind}Trigger` unit struct (implements the traits)
- `impl TriggerJobArgs for {Kind}Trigger` — sets `TRIGGER_KIND`, `Payload`, `v1_payload_fn`
Required in `handler_ee.rs`:
- `#[async_trait] impl TriggerCrud for {Kind}Trigger` with:
- `type Trigger = Trigger<{Kind}Config>`
- `type TriggerConfigRequest = {Kind}ConfigRequest`
- `const ROUTE_PREFIX: &'static str = "/{kind}_triggers";`
- `const TABLE_NAME`, `ADDITIONAL_SELECT_FIELDS`
- `get_deployed_object`, `validate_config`, `create_trigger`, `update_trigger`, `delete_trigger`, `test_connection`
- `additional_routes` (optional — mount extra endpoints for things like ARM resource listing, topic discovery)
Register the crate in `backend/Cargo.toml` as a workspace member and as a dep of `windmill-api` behind the feature flag.
## 3. Wire into `windmill-api` (feature-gated everywhere)
**`backend/windmill-api/src/triggers/handler.rs`** — mount the trigger crate:
```rust
#[cfg(all(feature = "enterprise", feature = "{kind}_trigger", feature = "private"))]
{
use crate::triggers::{kind}::{Kind}Trigger;
router = router.nest({Kind}Trigger::ROUTE_PREFIX, complete_trigger_routes({Kind}Trigger));
}
```
**`backend/windmill-api/src/triggers/{kind}/mod.rs`** — re-export the crate:
```rust
pub use windmill_trigger_{kind}::*;
```
**`backend/windmill-api/src/lib.rs`** — if the trigger receives inbound pushes, add a webhook route:
```rust
.nest("/{kind}/w/{workspace_id}", {
#[cfg(all(feature = "enterprise", feature = "{kind}_trigger", feature = "private"))]
{ triggers::{kind}::handler_oss::{kind}_push_route_handler() }
#[cfg(not(...))]
{ Router::new() }
})
```
## 4. `TriggerKind` enum (`backend/windmill-types/src/triggers.rs`)
Already has slots for most triggers but verify your variant exists:
- Add `{Kind}` to the `TriggerKind` enum
- Add match arm in `to_key()`
- Add match arm in `from_str`
- Add match arm in `JobTriggerKind` (if jobs need kind tagging)
## 5. OpenAPI (`backend/windmill-api/openapi.yaml`)
This file is huge and the single most-forgotten place. Add:
- `/w/{workspace}/{kind}_triggers/create` + `/update/{path}` + `/delete/{path}` + `/get/{path}` + `/list` + `/exists/{path}` + `/setmode/{path}` + `/test` paths (mirror gcp section)
- Any `additional_routes` your handler exposes (resource discovery, etc.)
- Schemas: `{Kind}Trigger`, `{Kind}TriggerData`, `{Kind}Mode` (if enum), `{Kind}DeliveryConfig`, helper request/response types
- Add `{kind}` to `CaptureTriggerKind` enum
- Add `{kind}_used: boolean` to the `UsedTriggers` response schema
Regenerate frontend client: `npm run generate-backend-client` from `frontend/`.
## 6. `UsedTriggers` + workspace export
**`backend/windmill-api-workspaces/src/workspaces.rs`** — add `{kind}_used: bool` to the `UsedTriggers` struct and add an `EXISTS(SELECT 1 FROM {kind}_trigger …)` to the `get_used_triggers` query.
**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON).
## 6.5 Hardcoded trigger-kind arrays (silent-failure hotspots)
Several files keep **hardcoded arrays** of trigger kind strings. Miss one and ACL checks / user offboarding / trash drop your kind:
- **`backend/windmill-api-groups/src/granular_acls.rs`** — `KINDS: [&str; N]`. **Increment N** (the compile error is cryptic otherwise). Controls which kinds accept granular ACL operations.
- **`backend/windmill-api-users/src/users.rs`** (`extra_perms_tables`) — which tables get `extra_perms` entries cleaned when a user is deleted.
- **`backend/windmill-api/src/offboarding.rs`** — three separate arrays (enumeration, fork-copy, and delete paths). **All three** need the new kind.
- **`backend/windmill-api/src/trash.rs`** — `valid_tables` for the trash / restore API.
- **`backend/windmill-git-sync/src/lib.rs`** — add a test assertion for `DeployedObject::{Kind}Trigger.get_kind() == "{kind}_trigger"` (the `get_kind` match arm itself lives in the enum impl — already required by the Rust compiler).
- **`backend/windmill-api-auth/src/scopes.rs`** — add the `{Kind}Triggers` variant to `ScopeDomain` enum + `as_str` match + `from_str` match. Required for the OAuth/token system to recognise `{kind}_triggers:read|write` scopes.
- **`backend/windmill-api/src/token.rs`** (`build_trigger_scope_domains``TRIGGER_DOMAINS`) — add `("{kind}_triggers", "{Kind display name}")` so the CreateToken UI's scope selector surfaces the `read` / `write` checkboxes.
**OpenAPI enums** to extend (do NOT forget — generated client will allow it but server rejects as 400):
- `CaptureTriggerKind` enum
- Three `kind` enums under `/w/{workspace}/acls/{get,add,remove}/{kind}/{path}` (yes, same list repeated three times)
After editing any of these, run a full `cargo check` with your feature flag + `gcp_trigger` + other core flags — the `KINDS: [&str; N]` length mismatch only surfaces when the crate compiles.
## 7. Capture infrastructure (`backend/windmill-api/src/capture.rs`)
If the trigger supports push delivery, it also needs a capture endpoint so users can test it:
- `{Kind}TriggerConfig` struct (gated by feature flags)
- `TriggerConfig::{Kind}` variant
- `set_{kind}_trigger_config` function (creates the subscription/equivalent pointing at the capture URL — use your `manage_{kind}_subscription` helper with `trigger_mode=false`)
- Both real + no-op versions behind feature gates
- `TriggerKind::{Kind} => set_{kind}_trigger_config(...)` arm in `set_config`
- `{kind}_payload` async handler — validates auth (if any), processes payload, calls `insert_capture_payload`
- Route: `.route("/{kind}/{runnable_kind}/{*path}", post({kind}_payload))` inside `workspaced_unauthed_service` — and expand the surrounding `#[cfg(any(...))]` to include your feature flag
## 8. CLI (`cli/`) — easy to miss, breaks sync silently
Check all of these:
**`cli/src/types.ts`:**
- Add `"{kind}"` to `TRIGGER_TYPES` array
- Add `"{kind}_trigger"` to `getTypeStrFromPath` return union
- Add match case in `getTypeStrFromPath`'s `typeEnding ===` chain
- Add `pushTrigger("{kind}", ...)` branch in `pushObj`
**`cli/src/commands/trigger/trigger.ts`:**
- Import `{Kind}Trigger` type
- Add `{kind}: {Kind}Trigger` to the `Trigger` type map
- Add `{kind}: wmill.get{Kind}Trigger`, `update{Kind}Trigger`, `create{Kind}Trigger` to each function map
- Add `{kind}: { ... }` template to `triggerTemplates`
- Add `list{Kind}Triggers` call + spread in the `list` aggregation
- Update `--kind` option descriptions to mention the new kind
**`cli/src/commands/sync/sync.ts`:**
- Add `path.endsWith(".{kind}_trigger" + ext)` in the file-type filter
- Add `typ == "{kind}_trigger"` in `getTypeOrder`
- Add `"{kind}_trigger"` to the delete-suffix regex (~line 3092)
- Add a `case "{kind}_trigger"` in the delete switch
**`cli/src/guidance/skills.ts`** — **DO NOT EDIT DIRECTLY**. It's auto-generated by `system_prompts/generate.py`. Instead:
- Edit `system_prompts/utils.py` → append `('{Kind}Trigger', '{kind}_trigger')` to the `SCHEMA_MAPPINGS['triggers']` list (this is the master list — the one in `generate.py` is duplicated and `utils.py` wins)
- Then run `python3 system_prompts/generate.py` — it regenerates `cli/src/guidance/skills.ts` with the schema extracted from `backend/windmill-api/openapi.yaml`
- Commit the regenerated file
## 9. Frontend — editor + drawer
Under `frontend/src/lib/components/triggers/{kind}/`:
- `{Kind}TriggerPanel.svelte` — the tile shown in the triggers listing
- `{Kind}TriggerEditor.svelte` — outer drawer wrapper
- `{Kind}TriggerEditorInner.svelte` — state + business logic; must expose:
- `openEdit(path, isFlow, defaultValues?)` method
- `isEditor` prop, `onConfigChange` + `onCaptureConfigChange` callbacks
- `get{Kind}Config()` + `get{Kind}CaptureConfig()` helpers
- `captureConfig = $derived.by(untrack(() => isEditor) ? get{Kind}CaptureConfig : () => ({}))`
- `$effect(() => { const args = [captureConfig, isValid] as const; untrack(() => onCaptureConfigChange?.(...args)) })`
- `{Kind}TriggerEditorConfigSection.svelte` — form fields; use design-system components (`TextInput`, `Select`, `Toggle`, `ToggleButtonGroup`), never raw `<input>`
- `{Kind}Capture.svelte` — capture panel; wraps `CaptureSection` with `captureType="{kind}"`
- `utils.ts``requestBody` builders and any trigger-type-specific helpers
## 10. Frontend — global integration
Easy to miss:
- **`frontend/src/lib/components/triggers.ts`** — add `'{kind}'` to the `TriggerKind` union
- **`frontend/src/lib/components/triggers/CaptureWrapper.svelte`**:
- Import `{Kind}Capture`
- Add to `isStreamingCapture()` array (streaming = pull-style; push-style is typically `false`)
- Add `{:else if captureType === '{kind}'}` branch with the `<{Kind}Capture>` render
- **`frontend/src/lib/components/sidebar/SidebarContent.svelte`** — import the icon, add the nav entry
- **`frontend/src/lib/components/sidebar/OperatorMenu.svelte`** — add the operator-mode entry
- **`frontend/src/routes/(root)/(logged)/+layout.svelte`** — destructure `{kind}_used` from `/get_used_triggers` response, push `'{kind}'` into `usedKinds`
- **`frontend/src/lib/components/search/GlobalSearchModal.svelte`** — import icon, add "Go to {Kind} ..." entry
- **`frontend/src/lib/components/offboarding-utils.ts`** — add mappings `{kind}_trigger: '{kind}_triggers'` and `{kind}_trigger: '{kind} trigger'`
- **`frontend/src/lib/components/icons/{Kind}Icon.svelte`** — single-path SVG, `fill={color ?? 'currentColor'}`, `size` prop default 16 (match existing icons — don't hardcode colors, don't use `width`/`height` props)
- **`frontend/src/routes/(root)/(logged)/{kind}_triggers/+page.svelte`** — listing page (mirror `gcp_triggers/+page.svelte` for push+pull, `kafka_triggers` for pure streaming)
- **`frontend/src/lib/components/CompareWorkspaces.svelte`** — workspace fork / compare tool. Needs: service import, editor import, `{kind}Editor` `$state`, `case '{kind}'` in `openTriggerDetails()`, entry in `triggerServices` object (list/delete/normalize), and `<{Kind}TriggerEditor bind:this={{kind}Editor} />` in the template
## 10.5 AI system prompts (`system_prompts/`)
- **`system_prompts/utils.py`** — append `('{Kind}Trigger', '{kind}_trigger')` to `SCHEMA_MAPPINGS['triggers']` (master list used by code generation + CLI skills)
- **`system_prompts/generate.py`** — also has a duplicated `schema_types` list (~line 903) for the AI `triggers` skill content. Add `('{Kind}Trigger', '{kind}_trigger')` there too
- **`system_prompts/generate.py`** `schema_names` (~line 1192) — add `'{Kind}Trigger'` (add `'New{Kind}Trigger'` only if the OpenAPI declares one; GCP and Azure don't)
- Run `python3 system_prompts/generate.py` — this rewrites `cli/src/guidance/skills.ts` and all `auto-generated/` docs. Commit the regenerated files
## 11. Validation
Run all of these before declaring done:
```bash
# Backend
cd backend
cargo check --features enterprise,{kind}_trigger,private # minimal
cargo check --features enterprise,azure_trigger,private,gcp_trigger,http_trigger,mqtt_trigger,postgres_trigger,sqs_trigger,kafka,nats,smtp,websocket # full
# SQLx offline data (never run `cargo sqlx prepare` directly — use the wrapper)
./update_sqlx.sh
# Frontend
cd frontend
npm run generate-backend-client
npm run check:fast
```
Smoke test in the UI: create a trigger, save, check it appears in sidebar + search, delete, re-create via CLI `wmill sync`.
## 12. Common pitfalls
- **Forgetting feature gates in `workspaced_unauthed_service()`** — the surrounding `#[cfg(any(...))]` expression must include your feature flag, not just the inner `#[cfg]` on the route
- **`.route(path, ...).route(path, ...)` with same path and different methods** — older axum replaced; use `.route(path, post(h1).options(h2))` to chain methods on the same `MethodRouter`
- **`on:event` directives** — legacy Svelte 4, no-op in runes mode. Use callback props (`onSelected`, `onConfigChange`)
- **`$bindable(default_value)` on optional props** — banned by project CLAUDE.md. Use `$bindable()` + `$derived(prop ?? default)` instead
- **CORS layer intercepting OPTIONS** — tower-http CorsLayer short-circuits OPTIONS before reaching your handler. For server-to-server webhook endpoints, drop the CORS layer entirely (CORS is browser-only)
- **DeliveryAttributeMappings / custom headers for auth** — prefer HMAC or sha256-hashed shared secrets over opaque JWTs when the provider doesn't support signed tokens natively. Store only the hash; regenerate secret on every save
- **ARM / API resource-listing cascades** — if the trigger's resource type is deep (Azure: subscription → RG → namespace → topic), offer dropdowns in the UI populated from the provider's APIs using the user's credential resource
- **Clearing stale selections on dependency change** — when a dropdown's underlying data reloads (e.g., user changes SP or edition), clear selections that no longer match the new list
- **Workspace-scoped tag compatibility** — if the trigger has tags, verify forked workspaces handle them (see commit `0773b5bc85` for a historical fix)
## 13. EE file split
If the trigger is enterprise-only, the code lives in `windmill-ee-private__worktrees/.../windmill-trigger-{kind}/src/*_ee.rs` and is symlinked into the OSS tree. The `windmill-ee-private__worktrees/` directory holds the real files; changes propagate via symlinks. See `docs/enterprise.md` for the workflow.
## 14. Final checklist before PR
- [ ] Migration up/down tested (revert + re-apply)
- [ ] `./update_sqlx.sh` committed the updated `.sqlx/` offline data
- [ ] `cargo check` passes with your feature flag + with all trigger features
- [ ] `npm run check:fast` passes
- [ ] Trigger visible in sidebar with correct icon weight (not oversized/colored — use `currentColor`)
- [ ] Create, edit, delete flow all work in the UI
- [ ] Capture button works (if push-capable)
- [ ] Trigger appears in `/get_used_triggers` → sidebar pulse
- [ ] `wmill sync pull` + `wmill sync push` both round-trip the trigger
- [ ] `wmill trigger list` includes it
- [ ] OpenAPI schemas are complete (no `null` in generated types)
+46
View File
@@ -1,5 +1,51 @@
# Changelog
## [1.690.0](https://github.com/windmill-labs/windmill/compare/v1.689.0...v1.690.0) (2026-04-23)
### Features
* add ai agent conversation output control ([#8915](https://github.com/windmill-labs/windmill/issues/8915)) ([9a60ff2](https://github.com/windmill-labs/windmill/commit/9a60ff2e77f197786f523755c3a9286a178a245c))
* add Azure Event Grid triggers ([#8888](https://github.com/windmill-labs/windmill/issues/8888)) ([d6c642b](https://github.com/windmill-labs/windmill/commit/d6c642b170b9547fe1d8db190affa35b305c9c8a))
* add OTEL_HOST_NAME env override for host.name attribute ([#8923](https://github.com/windmill-labs/windmill/issues/8923)) ([f429cb5](https://github.com/windmill-labs/windmill/commit/f429cb5e486aa5d2bd37f84c1fb30b9d350909e4))
### Bug Fixes
* **cli:** use wmill.yaml key consistently for workspace-specific items ([#8900](https://github.com/windmill-labs/windmill/issues/8900)) ([1722a7a](https://github.com/windmill-labs/windmill/commit/1722a7a2af5e00beeae204b78e588cd74a3ceb39))
* correct flow conversation pagination ([#8919](https://github.com/windmill-labs/windmill/issues/8919)) ([7fa924e](https://github.com/windmill-labs/windmill/commit/7fa924e67e212458726a839dbe366798b2709cd6))
* ensure schema is inferred on script/flow module load ([#8927](https://github.com/windmill-labs/windmill/issues/8927)) ([664d0f8](https://github.com/windmill-labs/windmill/commit/664d0f838d168978d7c27e88d2bb9019531e7ea1))
* include endpoint descriptions in mcp tools ([#8925](https://github.com/windmill-labs/windmill/issues/8925)) ([07951e8](https://github.com/windmill-labs/windmill/commit/07951e81ae9a1c26e8fe63bcd7a760b80500ca4c))
* load job metadata on approval page via approval token ([#8924](https://github.com/windmill-labs/windmill/issues/8924)) ([dac29e7](https://github.com/windmill-labs/windmill/commit/dac29e7d23d6e980c2b6fc4dd3a05a0d2e0170b3))
* slim app ai chat context ([#8922](https://github.com/windmill-labs/windmill/issues/8922)) ([132d8a6](https://github.com/windmill-labs/windmill/commit/132d8a61f9c109b2b447fab1a39565f52864b746))
## [1.689.0](https://github.com/windmill-labs/windmill/compare/v1.688.0...v1.689.0) (2026-04-22)
### Features
* add s3 stream progress logs to other DB executors ([#8898](https://github.com/windmill-labs/windmill/issues/8898)) ([2d4fadb](https://github.com/windmill-labs/windmill/commit/2d4fadb590590837412d638192fbd62bdc9331e8))
* allow hiding catalog picker and raw input on s3 form fields ([#8902](https://github.com/windmill-labs/windmill/issues/8902)) ([05baa4a](https://github.com/windmill-labs/windmill/commit/05baa4ab026a307267d11fb827f8abcc246d1ac0))
* async dep endpoints and queue-position logs in cli ([#8895](https://github.com/windmill-labs/windmill/issues/8895)) ([aaf3a19](https://github.com/windmill-labs/windmill/commit/aaf3a1974746be451adf463fd1f0e584b2fa995e))
* auto-strip UTF-8 BOM when reading local files in CLI ([#8911](https://github.com/windmill-labs/windmill/issues/8911)) ([99bc96d](https://github.com/windmill-labs/windmill/commit/99bc96d0b231a2af303b28aa87d5de9141ee5cab))
### Bug Fixes
* add aws-config to private feature to restore ce build ([18eed92](https://github.com/windmill-labs/windmill/commit/18eed92dd66d635305a72818e2fcf0ee8b9672cc))
* add flow conversation token scope ([#8903](https://github.com/windmill-labs/windmill/issues/8903)) ([aea7444](https://github.com/windmill-labs/windmill/commit/aea74445a31d30abb8030763db91e1829db772f0))
* add proxy eval coverage for gemini schemas ([#8897](https://github.com/windmill-labs/windmill/issues/8897)) ([fddd8e2](https://github.com/windmill-labs/windmill/commit/fddd8e288fc0fd7af3b1df1ddd4476fb57694ed3))
* apply powershell workspace dependencies to deployed scripts ([#8912](https://github.com/windmill-labs/windmill/issues/8912)) ([dc89673](https://github.com/windmill-labs/windmill/commit/dc896737ac1dcd90ab96314b2bc2f044ff833b8a))
* detect and clearly label OOM in zombie flow alerts ([#8901](https://github.com/windmill-labs/windmill/issues/8901)) ([680c711](https://github.com/windmill-labs/windmill/commit/680c711f9262683c046a78532b22be5f5a4121a8))
* omit default_permissioned_as from tarball export when empty ([bbb564c](https://github.com/windmill-labs/windmill/commit/bbb564c1420593014d17f352ba38d3ed38c248e1))
* persist flow groups from AI chat tool calls ([#8906](https://github.com/windmill-labs/windmill/issues/8906)) ([932d183](https://github.com/windmill-labs/windmill/commit/932d18331196ef3e87c45d8a06ae45ac8013bd7a))
* push parent resource on fileset child add/delete ([#8910](https://github.com/windmill-labs/windmill/issues/8910)) ([f29badc](https://github.com/windmill-labs/windmill/commit/f29badcf368e7c712f1515fa30a6a0e179a4bdc5))
* rust nsjail RUSTUP_HOME mount and arch-aware cache keys ([#8890](https://github.com/windmill-labs/windmill/issues/8890)) ([f8c916c](https://github.com/windmill-labs/windmill/commit/f8c916cb6073f5566289c395ec39ec3919f449e7))
* skip opus 4.7 sampling params ([#8904](https://github.com/windmill-labs/windmill/issues/8904)) ([1e83278](https://github.com/windmill-labs/windmill/commit/1e83278fe2ef5a5c6959a9351e7286d9dbf2453a))
* support windmill chat answer override ([#8909](https://github.com/windmill-labs/windmill/issues/8909)) ([eeb5d12](https://github.com/windmill-labs/windmill/commit/eeb5d12be3ba2aedf2ebc4843d4395e241ecc8d3))
* track dollar-quoted strings in SQL block splitter ([#8891](https://github.com/windmill-labs/windmill/issues/8891)) ([53badf1](https://github.com/windmill-labs/windmill/commit/53badf1a8cff576bb4ccbc75f045efb457b6a07d))
* trigger failure_module when branchone predicate throws ([#8905](https://github.com/windmill-labs/windmill/issues/8905)) ([dffb89e](https://github.com/windmill-labs/windmill/commit/dffb89e00632bd4e7bbe9998bccb1f14357d9c07)), closes [#8889](https://github.com/windmill-labs/windmill/issues/8889)
## [1.688.0](https://github.com/windmill-labs/windmill/compare/v1.687.0...v1.688.0) (2026-04-20)
@@ -33,7 +33,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -43,7 +43,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -0,0 +1,83 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at, created_seq, step_name, success\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at, created_seq, step_name, success\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_seq DESC\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_seq ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "conversation_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "message_type: MessageType",
"type_info": {
"Custom": {
"name": "message_type",
"kind": {
"Enum": [
"user",
"assistant",
"tool"
]
}
}
}
},
{
"ordinal": 3,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "job_id",
"type_info": "Uuid"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_seq",
"type_info": "Int8"
},
{
"ordinal": 7,
"name": "step_name",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "success",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true,
false,
false,
true,
false
]
},
"hash": "1c3473a0f9f6b6148b2c975f9f05bdefedf8a51c4e6ddf0eca367b9cc778d051"
}
@@ -37,7 +37,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -73,7 +74,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -42,7 +42,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -32,7 +32,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -39,7 +39,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -34,7 +34,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -74,7 +75,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT $2 OFFSET $3",
"query": "SELECT id, worker_group, event_type::text, desired_workers, reason, (applied_at AT TIME ZONE 'UTC') AS \"applied_at!: chrono::DateTime<chrono::Utc>\" FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -30,8 +30,8 @@
},
{
"ordinal": 5,
"name": "applied_at",
"type_info": "Timestamp"
"name": "applied_at!: chrono::DateTime<chrono::Utc>",
"type_info": "Timestamptz"
}
],
"parameters": {
@@ -47,8 +47,8 @@
null,
false,
true,
false
null
]
},
"hash": "a38df5d7dc4577c715d9acdaf87c38535ad388b1948a95efafd71135cfe5e3a6"
"hash": "5d26d8145464131740172082584b4e2f32987b67a125981e7122ae5b7b88fa46"
}
@@ -37,7 +37,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -159,7 +159,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -31,7 +31,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -126,7 +126,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -43,7 +43,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -29,7 +29,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\",\n EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS \"email_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'nextcloud'::native_trigger_service) AS \"nextcloud_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'google'::native_trigger_service) AS \"google_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'github'::native_trigger_service) AS \"github_used!\"\n ",
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\",\n EXISTS(SELECT 1 FROM azure_trigger WHERE workspace_id = $1) AS \"azure_used!\",\n EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS \"email_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'nextcloud'::native_trigger_service) AS \"nextcloud_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'google'::native_trigger_service) AS \"google_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'github'::native_trigger_service) AS \"github_used!\"\n ",
"describe": {
"columns": [
{
@@ -45,21 +45,26 @@
},
{
"ordinal": 8,
"name": "email_used!",
"name": "azure_used!",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "nextcloud_used!",
"name": "email_used!",
"type_info": "Bool"
},
{
"ordinal": 10,
"name": "google_used!",
"name": "nextcloud_used!",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "google_used!",
"type_info": "Bool"
},
{
"ordinal": 12,
"name": "github_used!",
"type_info": "Bool"
}
@@ -81,8 +86,9 @@
null,
null,
null,
null,
null
]
},
"hash": "952f244a06950ccfc70651cb48cdfb7766f7d62d34e68e65b975aaed9e104a5a"
"hash": "83d79dd52a708da7c0d55a171744214dc62651c8ae4ffa2be8b5aa68d8f8e791"
}
@@ -37,7 +37,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -34,7 +34,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -33,7 +33,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -189,7 +189,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -0,0 +1,53 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO azure_trigger (\n azure_resource_path, azure_mode, scope_resource_id, topic_name,\n subscription_name, event_type_filters,\n push_auth_config, workspace_id, path, script_path, is_flow,\n permissioned_as, mode, edited_by, email,\n error_handler_path, error_handler_args, retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7,\n $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
{
"Custom": {
"name": "azure_trigger_mode",
"kind": {
"Enum": [
"basic_push",
"namespace_push",
"namespace_pull"
]
}
}
},
"Text",
"Varchar",
"Varchar",
"Jsonb",
"Jsonb",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
{
"Custom": {
"name": "trigger_mode",
"kind": {
"Enum": [
"enabled",
"disabled",
"suspended"
]
}
}
},
"Varchar",
"Varchar",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "ad25201d0eea65972234cade87a95d8cd99fc26e5bd466942423cbd09efcebe4"
}
@@ -164,7 +164,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -109,7 +109,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -33,7 +33,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_at FROM flow_conversation_message WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "c67e81985093ff976f1326ff2254585e850f61a787a5b8f4a8d88f88016f1f2b"
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -109,7 +109,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -249,7 +249,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -27,7 +27,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -0,0 +1,53 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE azure_trigger SET\n azure_resource_path = $1,\n azure_mode = $2,\n scope_resource_id = $3,\n topic_name = $4,\n subscription_name = $5,\n event_type_filters = $6,\n push_auth_config = $7,\n is_flow = $8,\n edited_by = $9,\n permissioned_as = $10,\n script_path = $11,\n path = $12,\n mode = $13,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE workspace_id = $17 AND path = $18\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
{
"Custom": {
"name": "azure_trigger_mode",
"kind": {
"Enum": [
"basic_push",
"namespace_push",
"namespace_pull"
]
}
}
},
"Text",
"Varchar",
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "trigger_mode",
"kind": {
"Enum": [
"enabled",
"disabled",
"suspended"
]
}
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "d9ef1def7044e58722c5c69c31e9b9f6877de8bc7d4714915bbd572946d9270a"
}
@@ -189,7 +189,8 @@
"nextcloud",
"google",
"ci_test",
"github"
"github",
"azure"
]
}
}
@@ -33,7 +33,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at, step_name, success\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at, step_name, success\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n ",
"query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at, created_seq, step_name, success\n FROM flow_conversation_message\n WHERE conversation_id = $1\n AND created_seq > $2\n ORDER BY created_seq ASC\n LIMIT $3\n ",
"describe": {
"columns": [
{
@@ -46,11 +46,16 @@
},
{
"ordinal": 6,
"name": "created_seq",
"type_info": "Int8"
},
{
"ordinal": 7,
"name": "step_name",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "success",
"type_info": "Bool"
}
@@ -69,9 +74,10 @@
false,
true,
false,
false,
true,
false
]
},
"hash": "a739af2f72e117acc58374f6ed44f8223efa4826d6c10639f98e624474b247a3"
"hash": "e8802be9203c1e88a06e337260ccca029380139f89a01a89033e36a6ed9ac082"
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -26,7 +26,8 @@
"default_email",
"nextcloud",
"google",
"github"
"github",
"azure"
]
}
}
@@ -0,0 +1,100 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n azure_resource_path,\n script_path,\n is_flow,\n mode as \"mode: _\",\n workspace_id,\n path,\n edited_by,\n permissioned_as,\n push_auth_config AS \"push_auth_config: _\",\n retry as \"retry: _\",\n error_handler_path,\n error_handler_args as \"error_handler_args: _\"\n FROM\n azure_trigger\n WHERE\n workspace_id = $1 AND\n path = $2 AND\n azure_mode IN ('basic_push'::AZURE_TRIGGER_MODE, 'namespace_push'::AZURE_TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "azure_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "mode: _",
"type_info": {
"Custom": {
"name": "trigger_mode",
"kind": {
"Enum": [
"enabled",
"disabled",
"suspended"
]
}
}
}
},
{
"ordinal": 4,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "push_auth_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "retry: _",
"type_info": "Jsonb"
},
{
"ordinal": 10,
"name": "error_handler_path",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "error_handler_args: _",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true
]
},
"hash": "efdcdf0f8d24a23682bb3792ebaffeb7a8dc5632043b6bf4a3dd680c9b99b5df"
}
+572 -398
View File
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.688.0"
version = "1.690.0"
authors.workspace = true
edition.workspace = true
@@ -27,6 +27,7 @@ members = [
"./windmill-trigger-nats",
"./windmill-trigger-sqs",
"./windmill-trigger-gcp",
"./windmill-trigger-azure",
"./windmill-trigger-http",
"./windmill-native-triggers",
"./windmill-alerting",
@@ -86,7 +87,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.688.0"
version = "1.690.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -142,6 +143,7 @@ mqtt_trigger = ["windmill-api/mqtt_trigger"]
native_trigger = ["windmill-api/native_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
gcp_trigger = ["windmill-api/gcp_trigger"]
azure_trigger = ["windmill-api/azure_trigger"]
smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"]
license = ["windmill-api/license", "windmill-api-settings/license"]
oauth2 = ["windmill-api/oauth2"]
@@ -183,7 +185,7 @@ oss_core = [
ce_core = ["oss_core", "private", "operator"]
ee_core = [
"enterprise", "stripe", "prometheus", "cloud",
"kafka", "sqs_trigger", "nats", "gcp_trigger",
"kafka", "sqs_trigger", "nats", "gcp_trigger", "azure_trigger",
"jemalloc", "otel", "operator"
]
ee_server = ["enterprise_saml", "tantivy", "agent_worker_server", "local_reports"]
@@ -197,7 +199,7 @@ ee_rhel = ["ce_core", "ee_core", "kafka-gssapi", "all_languages"]
ee_windows = ["ce_core", "ee_core", "all_languages_windows"]
all_sqlx_features = ["all_languages", "enterprise", "enterprise_saml", "embedding", "parquet", "prometheus", "flow_testing",
"openidconnect", "cloud", "jemalloc", "tantivy", "sqlx", "kafka", "kafka-gssapi", "nats", "otel", "dind", "websocket", "http_trigger",
"postgres_trigger", "mcp", "mqtt_trigger", "sqs_trigger", "gcp_trigger", "smtp", "stripe",
"postgres_trigger", "mcp", "mqtt_trigger", "sqs_trigger", "gcp_trigger", "azure_trigger", "smtp", "stripe",
"license", "oauth2", "zip", "static_frontend", "scoped_cache", "agent_worker_server", "bedrock", "native_trigger", "quickjs",
"windmill-git-sync/all_sqlx_features"]
@@ -277,6 +279,7 @@ windmill-trigger-kafka.workspace = true
windmill-trigger-nats.workspace = true
windmill-trigger-sqs.workspace = true
windmill-trigger-gcp.workspace = true
windmill-trigger-azure.workspace = true
windmill-api-auth.workspace = true
axum.workspace = true
serde.workspace = true
@@ -327,6 +330,7 @@ windmill-trigger-email = { path = "./windmill-trigger-email" }
windmill-trigger-nats = { path = "./windmill-trigger-nats" }
windmill-trigger-sqs = { path = "./windmill-trigger-sqs" }
windmill-trigger-gcp = { path = "./windmill-trigger-gcp" }
windmill-trigger-azure = { path = "./windmill-trigger-azure" }
windmill-trigger-http = { path = "./windmill-trigger-http" }
windmill-native-triggers = { path = "./windmill-native-triggers" }
windmill-alerting = { path = "./windmill-alerting" }
+1 -1
View File
@@ -1 +1 @@
2c2b8dc99689f54b8cd916fb9472fd5698b09478
4128203739a973330599dacfb054203cf9832f3a
@@ -384,6 +384,19 @@ def schema_to_rust_value(schema: Optional[Dict[str, Any]]) -> str:
return "None"
return f"Some(serde_json::json!({json.dumps(schema, indent=8)}))"
def build_tool_description(operation: Dict[str, Any], method: str, path: str) -> str:
"""Build the MCP tool description from OpenAPI summary and description."""
summary = operation.get('summary', '').strip()
description = operation.get('description', '').strip()
if summary and description:
return f"{summary}: {description}".rstrip('.!? ')
if summary:
return summary
if description:
return description.rstrip('.!? ')
return f'{method.upper()} {path}'
def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Find all endpoints marked with x-mcp-tool: true."""
tools = []
@@ -395,7 +408,7 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
# Extract tool information
tool = {
'name': operation.get('operationId', f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"),
'description': operation.get('summary', operation.get('description', f'{method.upper()} {path}')),
'description': build_tool_description(operation, method, path),
'instructions': operation.get('x-mcp-instructions', ''),
'path': path,
'method': method.upper(),
@@ -607,4 +620,4 @@ def main():
print("Done!")
if __name__ == "__main__":
main()
main()
@@ -0,0 +1,5 @@
-- Add down migration script here
DROP TABLE IF EXISTS azure_trigger;
DROP TYPE IF EXISTS AZURE_TRIGGER_MODE;
-- Note: TRIGGER_KIND and JOB_TRIGGER_KIND enums cannot have values removed
-- so we leave 'azure' in place as a dead value (matches other trigger migrations)
@@ -0,0 +1,95 @@
-- Add up migration script here
ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'azure';
ALTER TYPE JOB_TRIGGER_KIND ADD VALUE IF NOT EXISTS 'azure';
CREATE TYPE AZURE_TRIGGER_MODE AS ENUM ('basic_push', 'namespace_push', 'namespace_pull');
CREATE TABLE azure_trigger (
azure_resource_path VARCHAR(255) NOT NULL,
azure_mode AZURE_TRIGGER_MODE NOT NULL,
scope_resource_id TEXT NOT NULL,
topic_name VARCHAR(255),
subscription_name VARCHAR(255) NOT NULL,
event_type_filters JSONB,
push_auth_config JSONB,
path VARCHAR(255) NOT NULL,
script_path VARCHAR(255) NOT NULL,
is_flow BOOLEAN NOT NULL,
workspace_id VARCHAR(50) NOT NULL,
edited_by VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
extra_perms JSONB NOT NULL DEFAULT '{}',
server_id VARCHAR(50),
last_server_ping TIMESTAMPTZ,
error TEXT,
mode TRIGGER_MODE NOT NULL DEFAULT 'enabled',
permissioned_as VARCHAR(255) NOT NULL,
error_handler_path VARCHAR(255),
error_handler_args JSONB,
retry JSONB,
labels TEXT[],
PRIMARY KEY (path, workspace_id),
CONSTRAINT azure_topic_name_matches_mode CHECK (
(azure_mode = 'basic_push' AND topic_name IS NULL) OR
(azure_mode IN ('namespace_push', 'namespace_pull') AND topic_name IS NOT NULL)
),
CONSTRAINT azure_push_auth_config_matches_mode CHECK (
(azure_mode IN ('basic_push', 'namespace_push') AND push_auth_config IS NOT NULL) OR
(azure_mode = 'namespace_pull' AND push_auth_config IS NULL)
)
);
CREATE UNIQUE INDEX unique_subscription_per_azure_scope
ON azure_trigger (subscription_name, scope_resource_id, workspace_id);
CREATE INDEX idx_azure_trigger_labels ON azure_trigger USING GIN (labels) WHERE labels IS NOT NULL;
GRANT ALL ON azure_trigger TO windmill_user;
GRANT ALL ON azure_trigger TO windmill_admin;
ALTER TABLE azure_trigger ENABLE ROW LEVEL SECURITY;
CREATE POLICY admin_policy ON azure_trigger FOR ALL TO windmill_admin USING (true);
CREATE POLICY see_folder_extra_perms_user_select ON azure_trigger FOR SELECT TO windmill_user
USING (SPLIT_PART(azure_trigger.path, '/', 1) = 'f' AND SPLIT_PART(azure_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_insert ON azure_trigger FOR INSERT TO windmill_user
WITH CHECK (SPLIT_PART(azure_trigger.path, '/', 1) = 'f' AND SPLIT_PART(azure_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_update ON azure_trigger FOR UPDATE TO windmill_user
USING (SPLIT_PART(azure_trigger.path, '/', 1) = 'f' AND SPLIT_PART(azure_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_delete ON azure_trigger FOR DELETE TO windmill_user
USING (SPLIT_PART(azure_trigger.path, '/', 1) = 'f' AND SPLIT_PART(azure_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_own ON azure_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(azure_trigger.path, '/', 1) = 'u' AND SPLIT_PART(azure_trigger.path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON azure_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(azure_trigger.path, '/', 1) = 'g' AND SPLIT_PART(azure_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_extra_perms_user_select ON azure_trigger FOR SELECT TO windmill_user
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
CREATE POLICY see_extra_perms_user_insert ON azure_trigger FOR INSERT TO windmill_user
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_update ON azure_trigger FOR UPDATE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_delete ON azure_trigger FOR DELETE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups_select ON azure_trigger FOR SELECT TO windmill_user
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
CREATE POLICY see_extra_perms_groups_insert ON azure_trigger FOR INSERT TO windmill_user
WITH CHECK (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_update ON azure_trigger FOR UPDATE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_delete ON azure_trigger FOR DELETE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
@@ -0,0 +1,10 @@
DROP INDEX IF EXISTS idx_conversation_message_conversation_created_seq;
CREATE INDEX IF NOT EXISTS idx_conversation_message_conversation_time
ON flow_conversation_message(conversation_id, created_at DESC);
ALTER TABLE flow_conversation_message
DROP CONSTRAINT IF EXISTS flow_conversation_message_created_seq_key;
ALTER TABLE flow_conversation_message
DROP COLUMN IF EXISTS created_seq;
@@ -0,0 +1,10 @@
ALTER TABLE flow_conversation_message
ADD COLUMN created_seq BIGINT GENERATED ALWAYS AS IDENTITY;
ALTER TABLE flow_conversation_message
ADD CONSTRAINT flow_conversation_message_created_seq_key UNIQUE (created_seq);
CREATE INDEX idx_conversation_message_conversation_created_seq
ON flow_conversation_message(conversation_id, created_seq);
DROP INDEX IF EXISTS idx_conversation_message_conversation_time;
+24 -24
View File
@@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill-common"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -6263,7 +6263,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"proc-macro2",
"quote",
@@ -6275,7 +6275,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"convert_case",
"serde",
@@ -6284,7 +6284,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6296,7 +6296,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6308,7 +6308,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"gosyn",
@@ -6320,7 +6320,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6332,7 +6332,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6344,7 +6344,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -6355,7 +6355,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6366,7 +6366,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6378,7 +6378,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6389,7 +6389,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -6411,7 +6411,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6423,7 +6423,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6437,7 +6437,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"convert_case",
@@ -6454,7 +6454,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6467,7 +6467,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"serde",
@@ -6479,7 +6479,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6497,7 +6497,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -6513,7 +6513,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6529,7 +6529,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"getrandom 0.2.17",
@@ -6561,7 +6561,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"serde",
@@ -6572,7 +6572,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.688.0"
version = "1.690.0"
dependencies = [
"anyhow",
"bitflags",
@@ -12,7 +12,7 @@ resolver = "2"
members = ["."]
[workspace.package]
version = "1.688.0"
version = "1.690.0"
edition = "2021"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
+4
View File
@@ -3879,6 +3879,10 @@ pub async fn reload_jwt_secret_setting(db: &DB) -> error::Result<()> {
JWT_SECRET.store(std::sync::Arc::new(jwt_secret));
// The debug signing key is derived from JWT_SECRET, so re-derive it here so
// rotation propagates to /api/debug/* signing without requiring a restart.
windmill_api::reload_debug_signing_key().await;
Ok(())
}
+1 -1
View File
@@ -80,7 +80,7 @@ flow: workspace_id(char), path(char), summary(text), description(text), value(js
FK: (workspace_id) -> workspace(id)
flow_conversation: id(uuid), workspace_id(char), flow_path(char), title(char), created_at(ts), updated_at(ts), created_by(char)
FK: (workspace_id) -> workspace(id)
flow_conversation_message: id(uuid), conversation_id(uuid), message_type(message_type), content(text), job_id(uuid), created_at(ts), step_name(char), success(bool)
flow_conversation_message: id(uuid), conversation_id(uuid), message_type(message_type), content(text), job_id(uuid), created_at(ts), created_seq(int8), step_name(char), success(bool)
FK: (conversation_id) -> flow_conversation(id) | (job_id) -> v2_job(id)
flow_iterator_data: job_id(uuid), itered(jsonb)
flow_node: id(bigint), workspace_id(char), hash(bigint), path(char), lock(text), code(text), flow(jsonb), hash_v2(char(64))
+3
View File
@@ -259,6 +259,7 @@ pub enum ScopeDomain {
MqttTriggers,
SqsTriggers,
GcpTriggers,
AzureTriggers,
PostgresTriggers,
EmailTriggers,
@@ -317,6 +318,7 @@ impl ScopeDomain {
Self::MqttTriggers => "mqtt_triggers",
Self::SqsTriggers => "sqs_triggers",
Self::GcpTriggers => "gcp_triggers",
Self::AzureTriggers => "azure_triggers",
Self::PostgresTriggers => "postgres_triggers",
Self::EmailTriggers => "email_triggers",
Self::NativeTriggers => "native_triggers",
@@ -366,6 +368,7 @@ impl ScopeDomain {
"mqtt_triggers" => Some(Self::MqttTriggers),
"sqs_triggers" => Some(Self::SqsTriggers),
"gcp_triggers" => Some(Self::GcpTriggers),
"azure_triggers" => Some(Self::AzureTriggers),
"postgres_triggers" => Some(Self::PostgresTriggers),
"email_triggers" => Some(Self::EmailTriggers),
"audit" => Some(Self::Audit),
+5 -2
View File
@@ -247,7 +247,7 @@ struct AutoscalingEvent {
event_type: Option<String>,
desired_workers: i32,
reason: Option<String>,
applied_at: chrono::NaiveDateTime,
applied_at: chrono::DateTime<chrono::Utc>,
}
async fn list_autoscaling_events(
@@ -260,9 +260,12 @@ async fn list_autoscaling_events(
}
let (per_page, offset) = windmill_common::utils::paginate(pagination);
// applied_at is a naive TIMESTAMP; reinterpret it as UTC so the response
// includes a timezone (otherwise the browser parses it as local time and
// TimeAgo clamps future timestamps to "0s ago").
let events = sqlx::query_as!(
AutoscalingEvent,
"SELECT id, worker_group, event_type::text, desired_workers, reason, applied_at FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT $2 OFFSET $3",
r#"SELECT id, worker_group, event_type::text, desired_workers, reason, (applied_at AT TIME ZONE 'UTC') AS "applied_at!: chrono::DateTime<chrono::Utc>" FROM autoscaling_event WHERE worker_group = $1 ORDER BY applied_at DESC LIMIT $2 OFFSET $3"#,
worker_group,
per_page as i64,
offset as i64
-1
View File
@@ -18,7 +18,6 @@ chrono.workspace = true
ed25519-dalek.workspace = true
hex.workspace = true
lazy_static.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
+51 -19
View File
@@ -37,7 +37,7 @@ use tokio::sync::RwLock;
use uuid::Uuid;
use windmill_audit::{audit_oss::audit_log, ActionKind};
use windmill_common::{
db::UserDB, error::JsonResult, jobs::JobKind, scripts::ScriptLang,
db::UserDB, error::JsonResult, jobs::JobKind, jwt::JWT_SECRET, scripts::ScriptLang,
users::username_to_permissioned_as,
};
@@ -48,35 +48,67 @@ pub const DEBUG_TOKEN_TTL_SECS: i64 = 60;
lazy_static::lazy_static! {
/// Ed25519 signing key for debug tokens.
/// Generated at startup if not provided via environment variable.
///
/// Derived deterministically from the instance `JWT_SECRET` so all API
/// replicas agree on the same key without coordination. Refreshed via
/// [`reload_debug_signing_key`] when `JWT_SECRET` is reloaded.
static ref DEBUG_SIGNING_KEY: Arc<RwLock<Option<SigningKey>>> = Arc::new(RwLock::new(None));
}
/// Initialize the debug signing key.
/// Call this at server startup.
pub async fn init_debug_signing_key() {
let mut key_guard = DEBUG_SIGNING_KEY.write().await;
/// Domain-separation tag so the debug Ed25519 seed cannot be confused with
/// any other HMAC/HS256 usage of `JWT_SECRET`.
const DEBUG_KEY_DERIVATION_TAG: &[u8] = b"windmill-debug-signing-key:v1:";
// Check if key is provided via environment variable (base64-encoded seed)
fn derive_signing_key_from_jwt_secret(jwt_secret: &str) -> SigningKey {
let mut hasher = Sha256::new();
hasher.update(DEBUG_KEY_DERIVATION_TAG);
hasher.update(jwt_secret.as_bytes());
let seed: [u8; 32] = hasher.finalize().into();
SigningKey::from_bytes(&seed)
}
fn compute_debug_signing_key() -> Option<SigningKey> {
// Env var override: base64url-encoded 32-byte seed. Useful for tests or
// advanced deployments that want to pin the key independently.
if let Ok(seed_b64) = std::env::var("DEBUG_SIGNING_KEY_SEED") {
if let Ok(seed_bytes) = URL_SAFE_NO_PAD.decode(&seed_b64) {
if seed_bytes.len() >= 32 {
match URL_SAFE_NO_PAD.decode(&seed_b64) {
Ok(seed_bytes) if seed_bytes.len() >= 32 => {
let mut seed = [0u8; 32];
seed.copy_from_slice(&seed_bytes[..32]);
*key_guard = Some(SigningKey::from_bytes(&seed));
tracing::info!("Debug signing key loaded from environment");
return;
tracing::info!("Debug signing key loaded from DEBUG_SIGNING_KEY_SEED");
return Some(SigningKey::from_bytes(&seed));
}
_ => tracing::warn!(
"Invalid DEBUG_SIGNING_KEY_SEED (expect base64url-encoded 32+ bytes); falling back to JWT_SECRET derivation"
),
}
tracing::warn!("Invalid DEBUG_SIGNING_KEY_SEED, generating new key");
}
// Generate a new random key using rand
let mut seed = [0u8; 32];
rand::Rng::fill(&mut rand::rng(), &mut seed);
let signing_key = SigningKey::from_bytes(&seed);
tracing::info!("Generated new debug signing key");
*key_guard = Some(signing_key);
let jwt_secret = JWT_SECRET.load();
if jwt_secret.is_empty() {
return None;
}
Some(derive_signing_key_from_jwt_secret(&jwt_secret))
}
/// Initialize the debug signing key. Call once at server startup, after
/// `reload_jwt_secret_setting` so `JWT_SECRET` is populated.
pub async fn init_debug_signing_key() {
reload_debug_signing_key().await;
}
/// Recompute and store the debug signing key. Call after `JWT_SECRET` is
/// (re)loaded so rotation propagates without a pod restart.
pub async fn reload_debug_signing_key() {
let key = compute_debug_signing_key();
if key.is_none() {
tracing::warn!(
"Debug signing key not initialized: JWT_SECRET is empty and DEBUG_SIGNING_KEY_SEED is not set. /api/debug/* endpoints will return an error."
);
} else {
tracing::info!("Debug signing key initialized from JWT_SECRET");
}
*DEBUG_SIGNING_KEY.write().await = key;
}
pub fn global_service() -> Router {
@@ -33,6 +33,7 @@ pub struct FlowConversationMessage {
pub content: String,
pub job_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
pub created_seq: i64,
pub step_name: Option<String>,
pub success: bool,
}
@@ -40,7 +41,11 @@ pub struct FlowConversationMessage {
#[derive(Deserialize)]
pub struct ListConversationsQuery {
pub flow_path: Option<String>,
pub after_id: Option<Uuid>,
}
#[derive(Deserialize)]
pub struct ListMessagesQuery {
pub after_seq: Option<i64>,
}
async fn list_conversations(
@@ -68,15 +73,6 @@ async fn list_conversations(
if let Some(flow_path) = &query.flow_path {
sqlb.and_where_eq("flow_path", "?".bind(flow_path));
}
if let Some(after_id) = &query.after_id {
let message_id_created_at = sqlx::query_scalar!(
"SELECT created_at FROM flow_conversation_message WHERE id = $1",
after_id
)
.fetch_one(&mut *tx)
.await?;
sqlb.and_where_gt("created_at", "?".bind(&message_id_created_at.to_rfc3339()));
}
sqlb.order_by("updated_at", true)
.limit(per_page as i64)
@@ -157,6 +153,7 @@ async fn list_messages(
Extension(user_db): Extension<UserDB>,
Path((w_id, conversation_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
Query(query): Query<ListMessagesQuery>,
) -> JsonResult<Vec<FlowConversationMessage>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.clone().begin(&authed).await?;
@@ -178,25 +175,43 @@ async fn list_messages(
)));
}
// Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend
let messages = sqlx::query_as!(
FlowConversationMessage,
r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at, step_name, success
FROM (
SELECT id, conversation_id, message_type, content, job_id, created_at, step_name, success
FROM flow_conversation_message
WHERE conversation_id = $1
ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END
LIMIT $2 OFFSET $3
) AS messages
ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END
"#,
conversation_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut *tx)
.await?;
let messages = if let Some(after_seq) = query.after_seq {
sqlx::query_as!(
FlowConversationMessage,
r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at, created_seq, step_name, success
FROM flow_conversation_message
WHERE conversation_id = $1
AND created_seq > $2
ORDER BY created_seq ASC
LIMIT $3
"#,
conversation_id,
after_seq,
per_page as i64
)
.fetch_all(&mut *tx)
.await?
} else {
// Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend
sqlx::query_as!(
FlowConversationMessage,
r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at, created_seq, step_name, success
FROM (
SELECT id, conversation_id, message_type, content, job_id, created_at, created_seq, step_name, success
FROM flow_conversation_message
WHERE conversation_id = $1
ORDER BY created_seq DESC
LIMIT $2 OFFSET $3
) AS messages
ORDER BY created_seq ASC
"#,
conversation_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut *tx)
.await?
};
tx.commit().await?;
Ok(Json(messages))
@@ -24,7 +24,7 @@ use windmill_common::{
utils::{not_found_if_none, StripPath},
};
const KINDS: [&str; 19] = [
const KINDS: [&str; 20] = [
"script",
"group_",
"resource",
@@ -41,6 +41,7 @@ const KINDS: [&str; 19] = [
"postgres_trigger",
"mqtt_trigger",
"gcp_trigger",
"azure_trigger",
"sqs_trigger",
"email_trigger",
"volume",
@@ -883,6 +883,7 @@ async fn create_script_internal<'c>(
|| ns.language == ScriptLang::Java
|| ns.language == ScriptLang::Ruby
|| ns.language == ScriptLang::Rlang
|| ns.language == ScriptLang::Powershell
// for related places search: ADD_NEW_LANG
) {
Some(String::new())
+1
View File
@@ -1690,6 +1690,7 @@ pub async fn delete_workspace_user_internal(
"nats_trigger",
"sqs_trigger",
"gcp_trigger",
"azure_trigger",
"email_trigger",
];
for table in &extra_perms_tables {
@@ -3138,6 +3138,7 @@ struct UsedTriggers {
pub mqtt_used: bool,
pub sqs_used: bool,
pub gcp_used: bool,
pub azure_used: bool,
pub email_used: bool,
pub nextcloud_used: bool,
pub google_used: bool,
@@ -3162,6 +3163,7 @@ async fn get_used_triggers(
EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS "mqtt_used!",
EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS "sqs_used!",
EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS "gcp_used!",
EXISTS(SELECT 1 FROM azure_trigger WHERE workspace_id = $1) AS "azure_used!",
EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS "email_used!",
EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'nextcloud'::native_trigger_service) AS "nextcloud_used!",
EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'google'::native_trigger_service) AS "google_used!",
+4 -2
View File
@@ -10,8 +10,8 @@ path = "src/lib.rs"
[features]
default = []
private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-api-configs/private", "windmill-api-settings/private", "windmill-api-agent-workers?/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-email?/private", "windmill-git-sync/private", "windmill-autoscaling?/private"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-flows/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-schedule/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise"]
private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-api-configs/private", "windmill-api-settings/private", "windmill-api-agent-workers?/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-azure?/private", "windmill-trigger-email?/private", "windmill-git-sync/private", "windmill-autoscaling?/private"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-flows/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-schedule/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-azure?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise"]
stripe = []
run_inline = ["dep:windmill-worker", "windmill-api-configs/run_inline"]
agent_worker_server = ["dep:windmill-worker", "dep:windmill-api-agent-workers"]
@@ -37,6 +37,7 @@ mqtt_trigger = ["dep:windmill-trigger-mqtt", "windmill-store/mqtt_trigger"]
native_trigger = ["dep:windmill-native-triggers", "windmill-native-triggers/native_trigger", "dep:strum", "oauth2"]
sqs_trigger = ["dep:windmill-trigger-sqs", "windmill-store/sqs_trigger"]
gcp_trigger = ["dep:windmill-trigger-gcp", "windmill-store/gcp_trigger"]
azure_trigger = ["dep:windmill-trigger-azure", "windmill-store/azure_trigger"]
cloud = ["windmill-common/cloud", "windmill-api-auth/cloud", "windmill-store/cloud", "windmill-api-workspaces/cloud"]
mcp = ["dep:windmill-mcp", "windmill-mcp/server", "windmill-mcp/auth", "windmill-api-auth/mcp", "windmill-store/mcp"]
bedrock = ["windmill-ai/bedrock", "dep:aws-sdk-bedrock", "dep:aws-sdk-bedrockruntime", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-types"]
@@ -145,6 +146,7 @@ windmill-trigger-email = { workspace = true, optional = true }
windmill-trigger-nats = { workspace = true, optional = true }
windmill-trigger-sqs = { workspace = true, optional = true }
windmill-trigger-gcp = { workspace = true, optional = true }
windmill-trigger-azure = { workspace = true, optional = true }
windmill-trigger-http = { workspace = true, optional = true }
windmill-native-triggers = { workspace = true, optional = true }
windmill-alerting.workspace = true
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+486 -6
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.688.0
version: 1.690.0
title: Windmill API
contact:
@@ -4575,6 +4575,8 @@ paths:
type: boolean
gcp_used:
type: boolean
azure_used:
type: boolean
sqs_used:
type: boolean
email_used:
@@ -4593,6 +4595,7 @@ paths:
- postgres_used
- mqtt_used
- gcp_used
- azure_used
- sqs_used
- email_used
- nextcloud_used
@@ -5963,6 +5966,9 @@ paths:
- type
saml:
type: string
auto_login:
type: string
description: provider type to auto-redirect to on login (oauth key or "saml")
required:
- oauth
@@ -7406,6 +7412,9 @@ paths:
/w/{workspace}/scripts/create:
post:
summary: create script
description: |
Creates a new script when the path does not already exist.
Creates a new version of an existing script when called with the same path and the current `parent_hash`.
operationId: createScript
x-mcp-tool: true
x-mcp-instructions: "To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed."
@@ -9451,13 +9460,13 @@ paths:
schema:
type: string
format: uuid
- name: after_id
description: id to fetch only the messages after that id
- name: after_seq
description: Message sequence cursor to fetch only the messages after that cursor
in: query
required: false
schema:
type: string
format: uuid
type: integer
format: int64
responses:
"200":
description: conversation messages
@@ -11687,6 +11696,11 @@ paths:
in: query
schema:
type: boolean
- name: approval_token
in: query
description: Approval token granting read access to the job when not logged in. The token must be the one issued for this job's flow (i.e. the flow id used when generating the approval URL).
schema:
type: string
responses:
"200":
description: job details
@@ -15211,6 +15225,296 @@ paths:
items:
type: string
/w/{workspace}/azure_triggers/create:
post:
summary: create an Azure Event Grid trigger
operationId: createAzureTrigger
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AzureTriggerData"
responses:
"201":
description: azure trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/azure_triggers/update/{path}:
post:
summary: update an Azure Event Grid trigger
operationId: updateAzureTrigger
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AzureTriggerData"
responses:
"200":
description: azure trigger updated
content:
text/plain:
schema:
type: string
/w/{workspace}/azure_triggers/delete/{path}:
delete:
summary: delete an Azure Event Grid trigger
operationId: deleteAzureTrigger
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: azure trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/azure_triggers/get/{path}:
get:
summary: get an Azure Event Grid trigger
operationId: getAzureTrigger
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: azure trigger
content:
application/json:
schema:
$ref: "#/components/schemas/AzureTrigger"
/w/{workspace}/azure_triggers/list:
get:
summary: list azure triggers
operationId: listAzureTriggers
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: path
description: filter by exact path
in: query
schema:
type: string
- name: is_flow
in: query
schema:
type: boolean
- name: path_start
in: query
schema:
type: string
responses:
"200":
description: azure trigger list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/AzureTrigger"
/w/{workspace}/azure_triggers/exists/{path}:
get:
summary: check whether an azure trigger exists
operationId: existsAzureTrigger
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: true/false
content:
application/json:
schema:
type: boolean
/w/{workspace}/azure_triggers/setmode/{path}:
post:
summary: set azure trigger mode
operationId: setAzureTriggerMode
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
mode:
$ref: "#/components/schemas/TriggerMode"
required:
- mode
responses:
"200":
description: trigger mode updated
content:
text/plain:
schema:
type: string
/w/{workspace}/azure_triggers/test:
post:
summary: test Azure service principal connection
operationId: testAzureConnection
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TestAzureConnection"
responses:
"200":
description: connection successful
content:
text/plain:
schema:
type: string
/w/{workspace}/azure_triggers/namespaces/topics/list/{path}:
post:
summary: list topics under an Event Grid Namespace
operationId: listAzureNamespaceTopics
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AzureListTopics"
responses:
"200":
description: topic list
content:
application/json:
schema:
type: array
items:
type: object
/w/{workspace}/azure_triggers/namespaces/subscriptions/list/{path}:
post:
summary: list subscriptions under a Namespace topic
operationId: listAzureNamespaceSubscriptions
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AzureListSubscriptions"
responses:
"200":
description: subscription list
content:
application/json:
schema:
type: array
items:
type: object
/w/{workspace}/azure_triggers/subscriptions/delete/{path}:
delete:
summary: delete an Event Grid subscription on Azure
operationId: deleteAzureSubscription
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AzureDeleteSubscription"
responses:
"200":
description: subscription deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/azure_triggers/namespaces/list/{path}:
post:
summary: list Event Grid Namespaces the service principal can access
operationId: listAzureNamespaces
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: namespace list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/AzureArmResource"
/w/{workspace}/azure_triggers/basic/topics/list/{path}:
post:
summary: list Basic Event Grid topics + system topics the service principal can access
operationId: listAzureBasicTopics
tags:
- azure_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: topic list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/AzureArmResource"
/w/{workspace}/postgres_triggers/postgres/version/{path}:
get:
summary: get postgres version
@@ -17091,6 +17395,7 @@ paths:
postgres_trigger,
mqtt_trigger,
gcp_trigger,
azure_trigger,
sqs_trigger,
email_trigger,
volume,
@@ -17137,6 +17442,7 @@ paths:
postgres_trigger,
mqtt_trigger,
gcp_trigger,
azure_trigger,
sqs_trigger,
email_trigger,
volume,
@@ -17194,6 +17500,7 @@ paths:
postgres_trigger,
mqtt_trigger,
gcp_trigger,
azure_trigger,
sqs_trigger,
email_trigger,
volume,
@@ -20341,7 +20648,7 @@ components:
FlowConversationMessage:
type: object
required: [id, conversation_id, message_type, content, created_at]
required: [id, conversation_id, message_type, content, created_at, created_seq]
properties:
id:
type: string
@@ -20367,6 +20674,10 @@ components:
type: string
format: date-time
description: When the message was created
created_seq:
type: integer
format: int64
description: Monotonic cursor assigned when the message is inserted
step_name:
type: string
description: The step name that produced that message
@@ -22887,6 +23198,7 @@ components:
- mqtt
- sqs
- gcp
- azure
- google
- github
@@ -23374,6 +23686,8 @@ components:
type: number
gcp_count:
type: number
azure_count:
type: number
sqs_count:
type: number
nextcloud_count:
@@ -24048,6 +24362,171 @@ components:
required:
- subscription_id
AzureMode:
type: string
enum:
- basic_push
- namespace_push
- namespace_pull
description: "Azure Event Grid trigger mode."
AzureArmResource:
type: object
description: "An ARM resource the service principal can see."
properties:
id:
type: string
name:
type: string
location:
type: string
type:
type: string
required:
- id
- name
- type
AzureDeleteSubscription:
type: object
properties:
azure_mode:
$ref: "#/components/schemas/AzureMode"
scope_resource_id:
type: string
topic_name:
type: string
nullable: true
subscription_name:
type: string
required:
- azure_mode
- scope_resource_id
- subscription_name
AzureTrigger:
allOf:
- $ref: "#/components/schemas/TriggerExtraProperty"
type: object
description: "An Azure Event Grid trigger that executes a script or flow when events arrive."
properties:
azure_resource_path:
type: string
azure_mode:
$ref: "#/components/schemas/AzureMode"
scope_resource_id:
type: string
description: "ARM resource ID of the topic (basic) or namespace (namespace modes)."
topic_name:
type: string
nullable: true
description: "Topic name within the namespace (namespace modes only)."
subscription_name:
type: string
event_type_filters:
type: array
items:
type: string
nullable: true
server_id:
type: string
last_server_ping:
type: string
format: date-time
error:
type: string
error_handler_path:
type: string
error_handler_args:
$ref: "#/components/schemas/ScriptArgs"
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
required:
- azure_resource_path
- azure_mode
- scope_resource_id
- subscription_name
AzureTriggerData:
type: object
description: "Data for creating or updating an Azure Event Grid trigger."
properties:
azure_resource_path:
type: string
azure_mode:
$ref: "#/components/schemas/AzureMode"
scope_resource_id:
type: string
topic_name:
type: string
nullable: true
subscription_name:
type: string
base_endpoint:
type: string
description: "Base URL for push delivery endpoints (push modes only)."
event_type_filters:
type: array
items:
type: string
path:
type: string
script_path:
type: string
is_flow:
type: boolean
mode:
$ref: "#/components/schemas/TriggerMode"
error_handler_path:
type: string
error_handler_args:
$ref: "#/components/schemas/ScriptArgs"
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
permissioned_as:
type: string
preserve_permissioned_as:
type: boolean
labels:
type: array
items:
type: string
required:
- path
- script_path
- is_flow
- azure_resource_path
- azure_mode
- scope_resource_id
- subscription_name
TestAzureConnection:
type: object
properties:
azure_resource_path:
type: string
required:
- azure_resource_path
AzureListTopics:
type: object
properties:
scope_resource_id:
type: string
required:
- scope_resource_id
AzureListSubscriptions:
type: object
properties:
scope_resource_id:
type: string
topic_name:
type: string
required:
- scope_resource_id
- topic_name
AwsAuthResourceType:
type: string
enum:
@@ -26349,6 +26828,7 @@ components:
sqs,
mqtt,
gcp,
azure,
email,
]
+166 -2
View File
@@ -114,7 +114,8 @@ pub fn workspaced_unauthed_service() -> Router {
#[cfg(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
all(feature = "enterprise", feature = "gcp_trigger"),
all(feature = "enterprise", feature = "azure_trigger")
))]
{
#[cfg(feature = "http_trigger")]
@@ -125,12 +126,16 @@ pub fn workspaced_unauthed_service() -> Router {
#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))]
let router = router.route("/gcp/{runnable_kind}/{*path}", post(gcp_payload));
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
let router = router.route("/azure/{runnable_kind}/{*path}", post(azure_payload));
router
}
#[cfg(not(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
all(feature = "enterprise", feature = "gcp_trigger"),
all(feature = "enterprise", feature = "azure_trigger")
)))]
{
router
@@ -170,6 +175,26 @@ pub struct SqsTriggerConfig {
pub aws_auth_resource_type: AwsAuthResourceType,
}
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct AzureTriggerConfig {
pub azure_resource_path: String,
pub azure_mode: crate::triggers::azure::AzureMode,
pub scope_resource_id: String,
#[serde(default, deserialize_with = "empty_as_none")]
pub topic_name: Option<String>,
pub subscription_name: String,
#[serde(default, deserialize_with = "empty_as_none")]
pub base_endpoint: Option<String>,
#[serde(default)]
pub event_type_filters: Option<Vec<String>>,
/// Server-managed. Populated by `set_azure_trigger_config` after
/// `manage_azure_subscription` regenerates the secret; skipped on
/// (de)serialization so clients never see or send it.
#[serde(skip, default)]
pub push_auth_config: Option<crate::triggers::azure::PushAuthConfig>,
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct GcpTriggerConfig {
@@ -248,6 +273,8 @@ enum TriggerConfig {
Mqtt(MqttTriggerConfig),
#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))]
Gcp(GcpTriggerConfig),
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
Azure(AzureTriggerConfig),
#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))]
Email(EmailTriggerConfig),
}
@@ -419,6 +446,78 @@ async fn set_gcp_trigger_config(
Ok(capture_config)
}
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
async fn set_azure_trigger_config(
w_id: &str,
authed: ApiAuthed,
db: &DB,
mut capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
use crate::triggers::azure::{manage_azure_subscription, AzureConfigRequest};
let Some(TriggerConfig::Azure(azure_config)) = capture_config.trigger_config else {
return Err(Error::BadRequest(
"Invalid Azure Event Grid config".to_string(),
));
};
// Suffix subscription name so capture never clobbers the deployed trigger's
// subscription. Azure allows [A-Za-z0-9-]{3,50}; reserve 11 chars for
// "-wm-capture" (mirrors Kafka's `_wm_capture` convention — hyphen since
// Azure names disallow underscores).
let mut sub_name = azure_config.subscription_name;
if sub_name.len() > 39 {
sub_name.truncate(39);
}
sub_name.push_str("-wm-capture");
let mut req = AzureConfigRequest {
azure_resource_path: azure_config.azure_resource_path,
azure_mode: azure_config.azure_mode,
scope_resource_id: azure_config.scope_resource_id,
topic_name: azure_config.topic_name,
subscription_name: sub_name,
base_endpoint: azure_config.base_endpoint,
event_type_filters: azure_config.event_type_filters,
push_auth_config: azure_config.push_auth_config,
};
manage_azure_subscription(
authed,
db,
w_id,
&mut req,
&capture_config.path,
capture_config.is_flow,
false,
)
.await?;
capture_config.trigger_config = Some(TriggerConfig::Azure(AzureTriggerConfig {
azure_resource_path: req.azure_resource_path,
azure_mode: req.azure_mode,
scope_resource_id: req.scope_resource_id,
topic_name: req.topic_name,
subscription_name: req.subscription_name,
base_endpoint: req.base_endpoint,
event_type_filters: req.event_type_filters,
push_auth_config: req.push_auth_config,
}));
Ok(capture_config)
}
#[inline]
#[cfg(not(all(feature = "enterprise", feature = "azure_trigger", feature = "private")))]
async fn set_azure_trigger_config(
_w_id: &str,
_authed: ApiAuthed,
_db: &DB,
capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
Ok(capture_config)
}
async fn set_config(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -431,6 +530,7 @@ async fn set_config(
set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await?
}
TriggerKind::Gcp => set_gcp_trigger_config(&w_id, authed.clone(), &db, nc).await?,
TriggerKind::Azure => set_azure_trigger_config(&w_id, authed.clone(), &db, nc).await?,
_ => nc,
};
@@ -960,6 +1060,70 @@ async fn gcp_payload(
Ok(StatusCode::NO_CONTENT)
}
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
async fn azure_payload(
Extension(db): Extension<DB>,
Path((w_id, runnable_kind, path)): Path<(String, RunnableKind, String)>,
headers: HeaderMap,
request: Request,
) -> Result<StatusCode> {
use crate::triggers::azure::{
cloud_event_to_args, process_azure_push_request, validate_push_secret, AzureTrigger,
PushOutcome,
};
use crate::triggers::trigger_helpers::TriggerJobArgs;
let is_flow = matches!(runnable_kind, RunnableKind::Flow);
let (azure_trigger_config, owner, _email): (AzureTriggerConfig, _, _) =
get_capture_trigger_config_and_owner(&db, &w_id, &path, is_flow, &TriggerKind::Azure)
.await?;
// Basic handshake posts don't carry the secret header; let them through.
let is_classic_handshake = headers
.get("aeg-event-type")
.and_then(|h| h.to_str().ok())
.map(|v| v.eq_ignore_ascii_case("SubscriptionValidation"))
.unwrap_or(false);
if !is_classic_handshake {
let dc = azure_trigger_config
.push_auth_config
.as_ref()
.ok_or_else(|| {
Error::NotAuthorized("azure capture missing push_auth_config".to_string())
})?;
validate_push_secret(&headers, dc)?;
}
let outcome = process_azure_push_request(headers, request).await?;
let (cloud_events, headers_map) = match outcome {
PushOutcome::Handshake(_) => {
// Capture path doesn't need to echo validation response — return 200.
return Ok(StatusCode::OK);
}
PushOutcome::Events { cloud_events, headers } => (cloud_events, headers),
};
for event in cloud_events {
let (payload, trigger_info) = cloud_event_to_args(&event, &headers_map);
let (main_args, preprocessor_args) =
AzureTrigger::build_capture_payloads(&payload, trigger_info);
let _ = insert_capture_payload(
&db,
&w_id,
&path,
is_flow,
&TriggerKind::Azure,
main_args,
preprocessor_args,
&owner,
)
.await?;
}
Ok(StatusCode::NO_CONTENT)
}
#[cfg(feature = "http_trigger")]
async fn http_payload(
Extension(db): Extension<DB>,
+25 -4
View File
@@ -935,6 +935,7 @@ async fn list_selected_job_groups(
struct GetJobQuery {
pub no_logs: Option<bool>,
pub no_code: Option<bool>,
pub approval_token: Option<String>,
}
async fn get_job(
@@ -942,16 +943,36 @@ async fn get_job(
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetJobQuery { no_logs, no_code }): Query<GetJobQuery>,
Query(GetJobQuery { no_logs, no_code, approval_token }): Query<GetJobQuery>,
) -> error::Result<Response> {
let tags = opt_authed
.as_ref()
.map(|authed| get_scope_tags(authed))
.flatten();
let mut get = GetQuery::new()
.with_auth(&opt_authed)
.with_in_tags(tags.as_ref());
// A valid approval token on the same (workspace, flow) grants read access
// so the approval page can render job metadata without login. The approval
// URL usually carries the flow id directly — try that first and only
// resolve the parent flow if the direct check fails.
let has_valid_approval_token = if let Some(ref token) = approval_token {
if validate_approval_token(&db, token, id, &w_id).await.is_ok() {
true
} else if let Ok(flow_id) = get_flow_id_for_job(&db, id).await {
flow_id != id
&& validate_approval_token(&db, token, flow_id, &w_id)
.await
.is_ok()
} else {
false
}
} else {
false
};
let mut get = GetQuery::new().with_in_tags(tags.as_ref());
if !has_valid_approval_token {
get = get.with_auth(&opt_authed);
}
if no_code.unwrap_or(false) {
get = get.without_code();
+20
View File
@@ -216,6 +216,8 @@ pub use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
pub use windmill_common::utils::{COOKIE_DOMAIN, IS_SECURE};
pub use windmill_api_debug::reload_debug_signing_key;
#[cfg(feature = "oauth2")]
pub use windmill_oauth::OAUTH_CLIENTS;
@@ -898,6 +900,24 @@ pub async fn run_server(
Router::new()
}
})
.nest("/azure/w/{workspace_id}", {
#[cfg(all(
feature = "enterprise",
feature = "azure_trigger",
feature = "private"
))]
{
triggers::azure::handler_oss::azure_push_route_handler()
}
#[cfg(not(all(
feature = "enterprise",
feature = "azure_trigger",
feature = "private"
)))]
{
Router::new()
}
})
.route("/version", get(git_v))
.nest("/health/status", health::status_service())
.route("/min_keep_alive_version", get(min_keep_alive_version))
@@ -78,6 +78,12 @@ pub fn all_tools() -> Vec<EndpointTool> {
"type": "string",
"description": "The expiration date of the variable",
"format": "date-time"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@@ -157,6 +163,12 @@ pub fn all_tools() -> Vec<EndpointTool> {
"type": "string",
"description": "The new description of the variable"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"path__body": {
"type": "string",
"description": "The path to the variable (body parameter)"
@@ -244,6 +256,10 @@ pub fn all_tools() -> Vec<EndpointTool> {
"per_page": {
"type": "integer",
"description": "number of items to return for a given page (default 30, max 100)"
},
"label": {
"type": "string",
"description": "Filter by label"
}
},
"required": []
@@ -287,6 +303,12 @@ pub fn all_tools() -> Vec<EndpointTool> {
"resource_type": {
"type": "string",
"description": "The resource_type associated with the resource"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@@ -355,6 +377,12 @@ pub fn all_tools() -> Vec<EndpointTool> {
"type": "string",
"description": "The new resource_type to be associated with the resource"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"path__body": {
"type": "string",
"description": "The path to the resource (body parameter)"
@@ -437,6 +465,10 @@ pub fn all_tools() -> Vec<EndpointTool> {
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
},
"label": {
"type": "string",
"description": "Filter by label"
}
},
"required": []
@@ -544,6 +576,10 @@ pub fn all_tools() -> Vec<EndpointTool> {
"dedicated_worker": {
"type": "boolean",
"description": "(default regardless)\nIf true, show only scripts with dedicated_worker enabled.\nIf false, show only scripts with dedicated_worker disabled.\n"
},
"label": {
"type": "string",
"description": "Filter by label"
}
},
"required": []
@@ -555,7 +591,8 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
EndpointTool {
name: Cow::Borrowed("createScript"),
description: Cow::Borrowed("create script"),
description: Cow::Borrowed("create script: Creates a new script when the path does not already exist.
Creates a new version of an existing script when called with the same path and the current `parent_hash`"),
instructions: Cow::Borrowed("To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed."),
path: Cow::Borrowed("/w/{workspace}/scripts/create"),
method: Cow::Borrowed("POST"),
@@ -578,7 +615,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"language": {
"type": "string",
"description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative"
"description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative"
},
"kind": {
"type": "string",
@@ -772,6 +809,10 @@ pub fn all_tools() -> Vec<EndpointTool> {
"dedicated_worker": {
"type": "boolean",
"description": "(default regardless)\nIf true, show only flows with dedicated_worker enabled.\nIf false, show only flows with dedicated_worker disabled.\n"
},
"label": {
"type": "string",
"description": "Filter by label"
}
},
"required": []
@@ -1095,7 +1136,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"language": {
"type": "string",
"description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative"
"description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative"
},
"tag": {
"type": "string"
@@ -1127,7 +1168,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
},
"language": {
"type": "string",
"description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative"
"description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative"
},
"lock": {
"type": "string",
@@ -1691,6 +1732,12 @@ You should get the schema of the script or flow before creating the schedule to
"preserve_permissioned_as": {
"type": "boolean",
"description": "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@@ -1894,6 +1941,12 @@ You should get the schema of the script or flow before updating the schedule to
"type": "boolean",
"nullable": true,
"description": "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity"
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@@ -2001,6 +2054,10 @@ You should get the schema of the script or flow before updating the schedule to
"broad_filter": {
"type": "string",
"description": "broad search across multiple fields (case-insensitive substring match)"
},
"label": {
"type": "string",
"description": "Filter by label"
}
},
"required": []
+2 -1
View File
@@ -92,11 +92,12 @@ pub struct TokenResponse {
struct Logins {
oauth: Vec<String>,
saml: Option<String>,
auto_login: Option<String>,
}
#[cfg(not(feature = "private"))]
async fn list_logins() -> error::JsonResult<Logins> {
// Implementation is not open source
return Ok(Json(Logins { oauth: vec![], saml: None }));
return Ok(Json(Logins { oauth: vec![], saml: None, auto_login: None }));
}
#[allow(unused)]
+3
View File
@@ -189,6 +189,7 @@ async fn get_offboard_preview(
"nats_trigger",
"sqs_trigger",
"gcp_trigger",
"azure_trigger",
"email_trigger",
];
let mut triggers = HashMap::new();
@@ -748,6 +749,7 @@ async fn check_path_conflicts(
"nats_trigger",
"sqs_trigger",
"gcp_trigger",
"azure_trigger",
"email_trigger",
];
@@ -1020,6 +1022,7 @@ async fn offboard_user_from_workspace<'c>(
"nats_trigger",
"sqs_trigger",
"gcp_trigger",
"azure_trigger",
"email_trigger",
];
+1
View File
@@ -26,6 +26,7 @@ fn build_trigger_scope_domains() -> Vec<ScopeDomain> {
("mqtt_triggers", "MQTT"),
("sqs_triggers", "AWS SQS"),
("gcp_triggers", "GCP Pub/Sub"),
("azure_triggers", "Azure Event Grid"),
("postgres_triggers", "PostgreSQL"),
("email_triggers", "Email"),
];
+1
View File
@@ -469,6 +469,7 @@ async fn restore_trigger(tx: &mut sqlx::PgConnection, item: &TrashItemWithData)
"mqtt_trigger",
"sqs_trigger",
"gcp_trigger",
"azure_trigger",
"email_trigger",
];
@@ -0,0 +1 @@
pub use windmill_trigger_azure::*;
@@ -81,6 +81,16 @@ pub fn generate_trigger_routers() -> Router {
);
}
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
{
use crate::triggers::azure::AzureTrigger;
router = router.nest(
AzureTrigger::ROUTE_PREFIX,
complete_trigger_routes(AzureTrigger),
);
}
#[cfg(feature = "postgres_trigger")]
{
use crate::triggers::postgres::PostgresTrigger;
@@ -135,6 +145,7 @@ pub struct TriggersCount {
mqtt_count: i64,
sqs_count: i64,
gcp_count: i64,
azure_count: i64,
nextcloud_count: i64,
google_count: i64,
github_count: i64,
@@ -252,6 +263,17 @@ pub async fn get_triggers_count_internal(
#[cfg(not(all(feature = "gcp_trigger", feature = "enterprise", feature = "private")))]
let gcp_count = 0;
#[cfg(all(feature = "azure_trigger", feature = "enterprise", feature = "private"))]
let azure_count = {
use crate::triggers::azure::AzureTrigger;
let count = AzureTrigger
.trigger_count(&mut tx, w_id, is_flow, path)
.await;
count
};
#[cfg(not(all(feature = "azure_trigger", feature = "enterprise", feature = "private")))]
let azure_count = 0;
#[cfg(all(feature = "smtp", feature = "enterprise", feature = "private"))]
let email_count = {
use crate::triggers::email::EmailTrigger;
@@ -341,6 +363,7 @@ pub async fn get_triggers_count_internal(
postgres_count,
mqtt_count,
gcp_count,
azure_count,
sqs_count,
nextcloud_count,
google_count,
@@ -67,6 +67,14 @@ pub fn start_all_listeners(db: DB, killpill_rx: &tokio::sync::broadcast::Receive
listen_to(GcpTrigger, db.clone(), gcp_killpill_rx);
}
#[cfg(all(feature = "azure_trigger", feature = "enterprise", feature = "private"))]
{
let azure_killpill_rx = killpill_rx.resubscribe();
use crate::triggers::azure::AzureTrigger;
listen_to(AzureTrigger, db.clone(), azure_killpill_rx);
}
#[cfg(all(feature = "sqs_trigger", feature = "enterprise", feature = "private"))]
{
let gcp_killpill_rx = killpill_rx.resubscribe();
+2
View File
@@ -1,4 +1,6 @@
// Concrete trigger submodules (feature-gated)
#[cfg(all(feature = "azure_trigger", feature = "enterprise", feature = "private"))]
pub mod azure;
#[cfg(all(feature = "smtp", feature = "private"))]
pub mod email;
#[cfg(all(feature = "gcp_trigger", feature = "enterprise", feature = "private"))]
@@ -23,6 +23,7 @@ use crate::{apps::AppWithLastVersion, db::DB, folders::Folder};
feature = "kafka",
feature = "sqs_trigger",
feature = "gcp_trigger",
feature = "azure_trigger",
feature = "nats",
feature = "smtp",
),
@@ -237,6 +238,13 @@ where
if !preserve_extra_perms && obj.contains_key("extra_perms") {
obj.remove("extra_perms");
}
if obj
.get("default_permissioned_as")
.and_then(|v| v.as_array())
.is_some_and(|a| a.is_empty())
{
obj.remove("default_permissioned_as");
}
serde_json::to_string_pretty(&obj).ok()
})
@@ -761,6 +769,23 @@ pub(crate) async fn tarball_workspace(
}
}
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
{
use crate::triggers::azure::AzureTrigger;
let handler = AzureTrigger;
let azure_triggers = handler.list_triggers(&mut *tx, &w_id, None).await?;
for trigger in azure_triggers {
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
archive
.write_to_archive(
&trigger_str,
&format!("{}.azure_trigger.json", trigger.base.path),
)
.await?;
}
}
#[cfg(all(feature = "enterprise", feature = "nats", feature = "private"))]
{
use crate::triggers::nats::NatsTrigger;
+1 -1
View File
@@ -9,7 +9,7 @@ default = []
enterprise = ["dep:aws-config"]
instance_config_schema = ["dep:schemars"]
local_reports = ["dep:rsa", "dep:aes-gcm"]
private = ["dep:aws-sdk-rds", "dep:aws-sdk-secretsmanager"]
private = ["dep:aws-sdk-rds", "dep:aws-sdk-secretsmanager", "dep:aws-config"]
jemalloc = ["dep:tikv-jemalloc-ctl"]
tantivy = []
prometheus = ["dep:prometheus"]
@@ -50,6 +50,7 @@ pub const HUB_API_SECRET_SETTING: &str = "hub_api_secret";
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
pub const DISABLE_PASSWORD_LOGIN_SETTING: &str = "disable_password_login";
pub const AUTO_LOGIN_PROVIDER_SETTING: &str = "auto_login_provider";
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
pub const HUB_ACCESSIBLE_URL_SETTING: &str = "hub_accessible_url";
pub const DISABLE_HUB_SETTING: &str = "disable_hub";
+1 -1
View File
@@ -157,7 +157,7 @@ pub enum ObjectType {
WorkspaceDependencies,
}
pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28191/sync-script-to-git-repo-windmill";
pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28213/sync-script-to-git-repo-windmill";
/// Prefix used to identify fork workspaces. A workspace whose id starts with this string is a
/// fork of another workspace.
+8
View File
@@ -39,6 +39,7 @@ pub enum DeployedObject {
MqttTrigger { path: String, parent_path: Option<String> },
SqsTrigger { path: String, parent_path: Option<String> },
GcpTrigger { path: String, parent_path: Option<String> },
AzureTrigger { path: String, parent_path: Option<String> },
EmailTrigger { path: String, parent_path: Option<String> },
Settings { setting_type: String },
Key { key_type: String },
@@ -67,6 +68,7 @@ impl DeployedObject {
DeployedObject::MqttTrigger { path, .. } => path.to_owned(),
DeployedObject::SqsTrigger { path, .. } => path.to_owned(),
DeployedObject::GcpTrigger { path, .. } => path.to_owned(),
DeployedObject::AzureTrigger { path, .. } => path.to_owned(),
DeployedObject::EmailTrigger { path, .. } => path.to_owned(),
DeployedObject::Settings { .. } => "settings.yaml".to_string(),
DeployedObject::Key { .. } => "encryption_key.yaml".to_string(),
@@ -107,6 +109,7 @@ impl DeployedObject {
DeployedObject::MqttTrigger { parent_path, .. } => parent_path.to_owned(),
DeployedObject::SqsTrigger { parent_path, .. } => parent_path.to_owned(),
DeployedObject::GcpTrigger { parent_path, .. } => parent_path.to_owned(),
DeployedObject::AzureTrigger { parent_path, .. } => parent_path.to_owned(),
DeployedObject::EmailTrigger { parent_path, .. } => parent_path.to_owned(),
DeployedObject::Settings { .. } => None,
DeployedObject::Key { .. } => None,
@@ -135,6 +138,7 @@ impl DeployedObject {
DeployedObject::MqttTrigger { .. } => "mqtt_trigger",
DeployedObject::SqsTrigger { .. } => "sqs_trigger",
DeployedObject::GcpTrigger { .. } => "gcp_trigger",
DeployedObject::AzureTrigger { .. } => "azure_trigger",
DeployedObject::EmailTrigger { .. } => "email_trigger",
DeployedObject::Settings { .. } => "settings",
DeployedObject::Key { .. } => "key",
@@ -369,6 +373,10 @@ mod tests {
DeployedObject::GcpTrigger { path: "t".to_string(), parent_path: None }.get_kind(),
"gcp_trigger"
);
assert_eq!(
DeployedObject::AzureTrigger { path: "t".to_string(), parent_path: None }.get_kind(),
"azure_trigger"
);
assert_eq!(
DeployedObject::EmailTrigger { path: "t".to_string(), parent_path: None }.get_kind(),
"email_trigger"
+1
View File
@@ -21,6 +21,7 @@ postgres_trigger = []
mqtt_trigger = []
sqs_trigger = []
gcp_trigger = []
azure_trigger = []
kafka = []
nats = []
openidconnect = ["windmill-common/openidconnect"]
+1
View File
@@ -1820,6 +1820,7 @@ async fn update_resource_type(
any(
feature = "sqs_trigger",
feature = "gcp_trigger",
feature = "azure_trigger",
feature = "kafka",
feature = "nats"
)
+43
View File
@@ -0,0 +1,43 @@
[package]
name = "windmill-trigger-azure"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_trigger_azure"
path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-common/enterprise", "windmill-store/enterprise", "windmill-trigger/enterprise"]
private = ["windmill-common/private", "windmill-store/private"]
[dependencies]
windmill-common = { workspace = true, default-features = false }
windmill-api-auth.workspace = true
windmill-store = { workspace = true, features = ["azure_trigger"] }
windmill-trigger.workspace = true
windmill-git-sync.workspace = true
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tracing.workspace = true
async-trait.workspace = true
itertools.workspace = true
anyhow.workspace = true
base64.workspace = true
bytes.workspace = true
http.workspace = true
quick_cache.workspace = true
lazy_static.workspace = true
reqwest.workspace = true
chrono.workspace = true
thiserror.workspace = true
sha2.workspace = true
rand.workspace = true
hex.workspace = true
constant_time_eq.workspace = true
@@ -0,0 +1,65 @@
#[allow(unused)]
#[cfg(feature = "private")]
pub use super::handler_ee::*;
#[cfg(not(feature = "private"))]
use {
super::AzureTrigger,
async_trait::async_trait,
sqlx::PgConnection,
windmill_api_auth::ApiAuthed,
windmill_common::{
error::{Error, Result},
DB,
},
windmill_git_sync::DeployedObject,
windmill_trigger::{TriggerCrud, TriggerData},
};
#[cfg(not(feature = "private"))]
#[async_trait]
impl TriggerCrud for AzureTrigger {
type Trigger = ();
type TriggerConfig = ();
type TriggerConfigRequest = ();
type TestConnectionConfig = ();
const TABLE_NAME: &'static str = "";
const TRIGGER_TYPE: &'static str = "";
const SUPPORTS_SERVER_STATE: bool = false;
const SUPPORTS_TEST_CONNECTION: bool = false;
const ROUTE_PREFIX: &'static str = "/azure_triggers";
const DEPLOYMENT_NAME: &'static str = "";
const IS_ALLOWED_ON_CLOUD: bool = false;
fn get_deployed_object(path: String, parent_path: Option<String>) -> DeployedObject {
DeployedObject::AzureTrigger { path, parent_path }
}
async fn create_trigger(
&self,
_db: &DB,
_executor: &mut PgConnection,
_authed: &ApiAuthed,
_w_id: &str,
_trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
Err(Error::BadRequest(
"Azure triggers are not available in open source version".to_string(),
))
}
async fn update_trigger(
&self,
_db: &DB,
_executor: &mut PgConnection,
_authed: &ApiAuthed,
_workspace_id: &str,
_path: &str,
_trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
Err(Error::BadRequest(
"Azure triggers are not available in open source version".to_string(),
))
}
}
+15
View File
@@ -0,0 +1,15 @@
#[cfg(feature = "private")]
mod handler_ee;
pub mod handler_oss;
#[cfg(feature = "private")]
mod listener_ee;
pub mod listener_oss;
#[cfg(feature = "private")]
mod mod_ee;
#[cfg(feature = "private")]
pub use mod_ee::*;
#[derive(Clone, Copy)]
pub struct AzureTrigger;
@@ -0,0 +1,52 @@
#[allow(unused)]
#[cfg(feature = "private")]
pub use super::listener_ee::*;
#[cfg(not(feature = "private"))]
use {
super::AzureTrigger,
serde_json::{value::RawValue, Value},
std::{collections::HashMap, sync::Arc},
tokio::sync::RwLock,
windmill_common::{error::Result, jobs::JobTriggerKind, triggers::TriggerKind, DB},
windmill_trigger::{listener::ListeningTrigger, trigger_helpers::TriggerJobArgs, Listener},
};
#[cfg(not(feature = "private"))]
impl TriggerJobArgs for AzureTrigger {
type Payload = Value;
const TRIGGER_KIND: TriggerKind = TriggerKind::Azure;
fn v1_payload_fn(_payload: &Self::Payload) -> HashMap<String, Box<RawValue>> {
HashMap::new()
}
}
#[cfg(not(feature = "private"))]
#[async_trait::async_trait]
impl Listener for AzureTrigger {
type Consumer = ();
type Extra = ();
type ExtraState = ();
const JOB_TRIGGER_KIND: JobTriggerKind = JobTriggerKind::Azure;
async fn get_consumer(
&self,
_db: &DB,
_listening_trigger: &ListeningTrigger<Self::TriggerConfig>,
_err_message: Arc<RwLock<Option<String>>>,
_killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> Result<Option<Self::Consumer>> {
Ok(None)
}
async fn consume(
&self,
_db: &DB,
_consumer: Self::Consumer,
_listening_trigger: &ListeningTrigger<Self::TriggerConfig>,
_err_message: Arc<RwLock<Option<String>>>,
_killpill_rx: tokio::sync::broadcast::Receiver<()>,
_extra_state: Option<&Self::ExtraState>,
) {
()
}
}
+43
View File
@@ -63,6 +63,10 @@ pub fn is_none_or_false(b: &Option<bool>) -> bool {
b.is_none() || !b.unwrap()
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Serialize, sqlx::FromRow)]
pub struct ListableFlow {
pub workspace_id: String,
@@ -921,6 +925,8 @@ pub enum FlowModuleValue {
AIAgent {
input_transforms: HashMap<String, InputTransform>,
tools: Vec<AgentTool>,
#[serde(default, skip_serializing_if = "is_false")]
omit_output_from_conversation: bool,
},
}
@@ -955,6 +961,7 @@ struct UntaggedFlowModuleValue {
modules_node: Option<FlowNodeId>,
assets: Option<Vec<AssetWithAltAccessType>>,
tools: Option<Vec<AgentTool>>,
omit_output_from_conversation: Option<bool>,
pass_flow_input_directly: Option<bool>,
squash: Option<bool>,
#[serde(flatten)]
@@ -1056,6 +1063,9 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
tools: untagged
.tools
.ok_or_else(|| serde::de::Error::missing_field("tools"))?,
omit_output_from_conversation: untagged
.omit_output_from_conversation
.unwrap_or(false),
}),
other => Err(serde::de::Error::unknown_variant(
other,
@@ -1173,4 +1183,37 @@ mod tests {
let val: FlowValue = serde_json::from_value(input).unwrap();
assert_eq!(val.modules.len(), 1);
}
#[test]
fn ai_agent_omit_output_from_conversation_defaults_to_false() {
let input = json!({
"type": "aiagent",
"tools": [],
"input_transforms": {}
});
let val: FlowModuleValue = serde_json::from_value(input).unwrap();
let FlowModuleValue::AIAgent { omit_output_from_conversation, .. } = val else {
panic!("expected aiagent module");
};
assert!(!omit_output_from_conversation);
}
#[test]
fn ai_agent_omit_output_from_conversation_preserves_true() {
let input = json!({
"type": "aiagent",
"tools": [],
"input_transforms": {},
"omit_output_from_conversation": true
});
let val: FlowModuleValue = serde_json::from_value(input).unwrap();
let FlowModuleValue::AIAgent { omit_output_from_conversation, .. } = val else {
panic!("expected aiagent module");
};
assert!(omit_output_from_conversation);
}
}
+2
View File
@@ -36,6 +36,7 @@ pub enum JobTriggerKind {
Postgres,
Schedule,
Gcp,
Azure,
Nextcloud,
Google,
Github,
@@ -58,6 +59,7 @@ impl std::fmt::Display for JobTriggerKind {
JobTriggerKind::Postgres => "postgres",
JobTriggerKind::Schedule => "schedule",
JobTriggerKind::Gcp => "gcp",
JobTriggerKind::Azure => "azure",
JobTriggerKind::Nextcloud => "nextcloud",
JobTriggerKind::Google => "google",
JobTriggerKind::Github => "github",
+3
View File
@@ -19,6 +19,7 @@ pub enum TriggerKind {
Sqs,
Postgres,
Gcp,
Azure,
Nextcloud,
Google,
Github,
@@ -38,6 +39,7 @@ impl TriggerKind {
TriggerKind::Sqs => "sqs".to_string(),
TriggerKind::Postgres => "postgres".to_string(),
TriggerKind::Gcp => "gcp".to_string(),
TriggerKind::Azure => "azure".to_string(),
TriggerKind::Nextcloud => "nextcloud".to_string(),
TriggerKind::Google => "google".to_string(),
TriggerKind::Github => "github".to_string(),
@@ -59,6 +61,7 @@ impl fmt::Display for TriggerKind {
TriggerKind::Sqs => "sqs",
TriggerKind::Postgres => "postgres",
TriggerKind::Gcp => "gcp",
TriggerKind::Azure => "azure",
TriggerKind::Nextcloud => "nextcloud",
TriggerKind::Google => "google",
TriggerKind::Github => "github",
+5
View File
@@ -75,6 +75,7 @@ pub struct ToolExecutionContext<'a> {
// Optional streaming & chat
pub stream_event_processor: Option<&'a StreamEventProcessor>,
pub flow_context: &'a mut FlowContext,
pub omit_output_from_conversation: bool,
pub previous_result: &'a Option<Box<RawValue>>,
pub id_context: &'a Option<crate::js_eval::IdContext>,
@@ -780,6 +781,10 @@ async fn add_tool_message_to_chat(
content: &str,
success: bool,
) {
if ctx.omit_output_from_conversation {
return;
}
let chat_enabled = ctx
.flow_context
.flow_status
+10 -4
View File
@@ -275,7 +275,9 @@ pub async fn handle_ai_agent_job(
let summary = module.summary.clone();
let FlowModuleValue::AIAgent { tools, .. } = module.get_value()? else {
let FlowModuleValue::AIAgent { tools, omit_output_from_conversation, .. } =
module.get_value()?
else {
return Err(Error::internal_err(
"AI agent module is not an AI agent".to_string(),
));
@@ -504,6 +506,7 @@ pub async fn handle_ai_agent_job(
killpill_rx,
has_stream,
has_websearch,
omit_output_from_conversation,
cancel_rx,
tool_abort_handles.clone(),
);
@@ -600,6 +603,7 @@ pub async fn run_agent(
killpill_rx: &mut tokio::sync::broadcast::Receiver<()>,
has_stream: &mut bool,
has_websearch: bool,
omit_output_from_conversation: bool,
// cancellation signal from parent
cancel_rx: tokio::sync::watch::Receiver<bool>,
@@ -860,6 +864,7 @@ pub async fn run_agent(
.as_ref()
.and_then(|fs| fs.chat_input_enabled)
.unwrap_or(false);
let persist_output_to_conversation = chat_enabled && !omit_output_from_conversation;
let step_name = get_step_name_from_flow(summary.as_deref(), effective_flow_step_id);
@@ -1061,7 +1066,7 @@ pub async fn run_agent(
agent_action: Some(AgentAction::WebSearch {}),
..Default::default()
});
if chat_enabled {
if persist_output_to_conversation {
if let Some(memory_id) = memory_id {
let agent_job_id = job.id;
let db_clone = db.clone();
@@ -1113,7 +1118,7 @@ pub async fn run_agent(
content = Some(OpenAIContent::Text(response_content.clone()));
// Add assistant message to conversation if chat_input_enabled
if chat_enabled && !response_content.is_empty() {
if persist_output_to_conversation && !response_content.is_empty() {
if let Some(memory_id) = memory_id {
let agent_job_id = job.id;
let db_clone = db.clone();
@@ -1195,6 +1200,7 @@ pub async fn run_agent(
killpill_rx,
stream_event_processor: stream_event_processor.as_ref(),
flow_context: &mut flow_context,
omit_output_from_conversation,
previous_result: &previous_result,
id_context: &id_context,
tool_abort_handles: tool_abort_handles.clone(),
@@ -1230,7 +1236,7 @@ pub async fn run_agent(
let content = to_raw_value(&s3_object);
// Add assistant message to conversation if chat_input_enabled
if chat_enabled {
if persist_output_to_conversation {
if let Some(memory_id) = memory_id {
let agent_job_id = job.id;
let db_clone = db.clone();
+24 -3
View File
@@ -6,7 +6,9 @@ use sqlx::types::Json;
use tokio::process::Command;
use windmill_common::client::AuthedClient;
use windmill_common::error::Error;
use windmill_common::scripts::ScriptLang;
use windmill_common::worker::{to_raw_value, write_file, Connection};
use windmill_common::workspace_dependencies::clean_lock_from_annotations;
use windmill_queue::{
append_logs, CanceledBy, MiniPulledJob, INIT_SCRIPT_PATH_PREFIX, PERIODIC_SCRIPT_PATH_PREFIX,
};
@@ -417,10 +419,16 @@ pub async fn handle_powershell_job(
// Resolve modules from workspace dependencies and/or script imports
let all_modules = match &maybe_lock {
MaybeLock::Resolved { lock } if !lock.is_empty() => {
// Deployed script with lock: parse workspace deps from lock, merge with script imports
let ws_modules = parse_modules_json(lock)?;
// Deployed script with lock: strip workspace-dependencies annotation header,
// parse the modules.json body, and merge with script imports.
let cleaned = clean_lock_from_annotations(lock, ScriptLang::Powershell);
let script_modules = parse_script_imports(content);
merge_module_requests(ws_modules, script_modules)
if cleaned.trim().is_empty() {
script_modules
} else {
let ws_modules = parse_modules_json(&cleaned)?;
merge_module_requests(ws_modules, script_modules)
}
}
MaybeLock::Unresolved { workspace_dependencies } => {
let script_modules = parse_script_imports(content);
@@ -1070,6 +1078,19 @@ mod tests {
assert!(parse_modules_json("not json").is_err());
}
#[test]
fn test_parse_modules_json_after_cleaning_header() {
// Simulates the deployed-script lock: workspace-dependencies header
// prepended to the modules.json content. `clean_lock_from_annotations`
// strips header lines so the remaining body can be JSON-parsed.
let lock = "# workspace-dependencies-mode: manual\n# workspace-dependencies: default:abc123\n{\"modules\": {\"PSWriteColor\": \"1.0.0\"}}";
let cleaned = clean_lock_from_annotations(lock, ScriptLang::Powershell);
let modules = parse_modules_json(&cleaned).unwrap();
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name, "PSWriteColor");
assert_eq!(modules[0].version, Some("1.0.0".to_string()));
}
// --- parse_script_imports tests ---
#[test]

Some files were not shown because too many files have changed in this diff Show More