{"owner":"windmill-labs","repo":"windmill","hasSkills":true,"totalSkillsCount":86,"totalTokensCount":41989,"categories":["anthropic-skill","root-instruction","claude-rule","codex-rule","subagent-persona","plugin-manifest"],"hasMcp":true,"mcpConfig":{"mcpServers":{"windmill":{"command":"npx","args":["-y","@modelcontextprotocol/server-windmill"]}}},"found":[".agents/skills/commit/SKILL.md",".agents/skills/domain-modeling/SKILL.md",".agents/skills/ai-chat/SKILL.md",".agents/skills/ai-evals/SKILL.md",".agents/skills/grill-me/SKILL.md",".agents/skills/codebase-design/SKILL.md",".agents/skills/adding-a-trigger/SKILL.md",".agents/skills/local-review/SKILL.md",".agents/skills/improve-codebase-architecture/SKILL.md",".agents/skills/local-review-codex/SKILL.md",".agents/skills/native-trigger/SKILL.md",".agents/skills/grilling/SKILL.md",".agents/skills/pr/SKILL.md",".agents/skills/refine/SKILL.md",".agents/skills/rust-backend/SKILL.md",".agents/skills/svelte-frontend/SKILL.md",".agents/skills/update-sqlx/SKILL.md",".claude/skills/adding-a-trigger/SKILL.md",".claude/skills/ai-chat/SKILL.md",".claude/skills/ai-evals/SKILL.md",".claude/skills/codebase-design/SKILL.md",".claude/skills/commit/SKILL.md",".claude/skills/domain-modeling/SKILL.md",".claude/skills/grill-me/SKILL.md",".claude/skills/grilling/SKILL.md",".claude/skills/improve-codebase-architecture/SKILL.md",".claude/skills/local-review-codex/SKILL.md",".claude/skills/local-review/SKILL.md",".claude/skills/native-trigger/SKILL.md",".claude/skills/pr/SKILL.md",".claude/skills/refine/SKILL.md",".claude/skills/rust-backend/SKILL.md",".claude/skills/svelte-frontend/SKILL.md",".claude/skills/update-sqlx/SKILL.md","system_prompts/auto-generated/skills/cli-commands/SKILL.md","system_prompts/auto-generated/skills/datatables/SKILL.md","system_prompts/auto-generated/skills/preview/SKILL.md","system_prompts/auto-generated/skills/raw-app/SKILL.md","system_prompts/auto-generated/skills/resources/SKILL.md","system_prompts/auto-generated/skills/schedules/SKILL.md","system_prompts/auto-generated/skills/triggers/SKILL.md","system_prompts/auto-generated/skills/workspace-settings/SKILL.md","system_prompts/auto-generated/skills/write-flow/SKILL.md","system_prompts/auto-generated/skills/write-pipeline/SKILL.md","system_prompts/auto-generated/skills/write-script-ansible/SKILL.md","system_prompts/auto-generated/skills/write-script-bash/SKILL.md","system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md","system_prompts/auto-generated/skills/write-script-bun/SKILL.md","system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md","system_prompts/auto-generated/skills/write-script-csharp/SKILL.md","system_prompts/auto-generated/skills/write-script-deno/SKILL.md","system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md","system_prompts/auto-generated/skills/write-script-go/SKILL.md","system_prompts/auto-generated/skills/write-script-graphql/SKILL.md","system_prompts/auto-generated/skills/write-script-java/SKILL.md","system_prompts/auto-generated/skills/write-script-mssql/SKILL.md","system_prompts/auto-generated/skills/write-script-mysql/SKILL.md","system_prompts/auto-generated/skills/write-script-php/SKILL.md","system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md","system_prompts/auto-generated/skills/write-script-powershell/SKILL.md","system_prompts/auto-generated/skills/write-script-python3/SKILL.md","system_prompts/auto-generated/skills/write-script-rlang/SKILL.md","system_prompts/auto-generated/skills/write-script-rust/SKILL.md","system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md","system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md","AGENTS.md","CLAUDE.md",".codex/config.toml","ai_evals/AGENTS.md","ai_evals/CLAUDE.md","backend/AGENTS.md","backend/CLAUDE.md","frontend/AGENTS.md","frontend/CLAUDE.md",".agents/skills/UPSTREAM.md",".claude/agents/branch-diff-reviewer.md",".claude/agents/openapi-sync.md","backend/generate_mcp_endpoints_tools/README.md",".agents/skills/codebase-design/DEEPENING.md",".agents/skills/codebase-design/DESIGN-IT-TWICE.md",".agents/skills/domain-modeling/CONTEXT-FORMAT.md",".agents/skills/improve-codebase-architecture/HTML-REPORT.md","ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml","ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml","ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml","ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml"],"skills":{".agents/skills/commit/SKILL.md":"---\nname: commit\nuser_invocable: true\ndescription: Create a git commit with conventional commit format. MUST use anytime you want to commit changes.\n---\n\n# Git Commit Skill\n\nCreate a focused, single-line commit following conventional commit conventions.\n\n## Instructions\n\n1. **Analyze changes**: Run `git status` and `git diff` to understand what was modified\n2. **Stage only modified files**: Add files individually by name. NEVER use `git add -A` or `git add .`\n3. **Write commit message**: Follow the conventional commit format as a single line\n\n## Conventional Commit Format\n\n```\n<type>: <description>\n```\n\n### Types\n- `feat`: New feature or capability\n- `fix`: Bug fix\n- `refactor`: Code change that neither fixes a bug nor adds a feature\n- `docs`: Documentation only changes\n- `style`: Formatting, missing semicolons, etc (no code change)\n- `test`: Adding or correcting tests\n- `chore`: Maintenance tasks, dependency updates, etc\n- `perf`: Performance improvement\n\n### Rules\n- Message MUST be a single line (no multi-line messages)\n- Description should be lowercase, imperative mood (\"add\" not \"added\")\n- No period at the end\n- Keep under 72 characters total\n\n### Examples\n```\nfeat: add token usage tracking for AI providers\nfix: resolve null pointer in job executor\nrefactor: extract common validation logic\ndocs: update API endpoint documentation\nchore: upgrade sqlx to 0.7\n```\n\n## Execution Steps\n\n1. Run `git status` to see all changes\n2. Run `git diff` to understand the changes in detail\n3. Run `git log --oneline -5` to see recent commit style\n4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`\n5. Create the commit with conventional format:\n   ```bash\n   git commit -m \"<type>: <description>\"\n   ```\n6. Run `git status` to verify the commit succeeded\n",".agents/skills/domain-modeling/SKILL.md":"---\nname: domain-modeling\ndescription: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, or when another skill needs to maintain the domain model.\n---\n\n# Domain Modeling\n\nActively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)\n\n## File structure\n\nMost repos have a single context:\n\n```\n/\n├── CONTEXT.md\n└── src/\n```\n\nIf a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:\n\n```\n/\n├── CONTEXT-MAP.md\n└── src/\n    ├── ordering/\n    │   └── CONTEXT.md\n    └── billing/\n        └── CONTEXT.md\n```\n\nCreate files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved.\n\n## During the session\n\n### Challenge against the glossary\n\nWhen the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. \"Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?\"\n\n### Sharpen fuzzy language\n\nWhen the user uses vague or overloaded terms, propose a precise canonical term. \"You're saying 'account' — do you mean the Customer or the User? Those are different things.\"\n\n### Discuss concrete scenarios\n\nWhen domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.\n\n### Cross-reference with code\n\nWhen the user states how something works, check whether the code agrees. If you find a contradiction, surface it: \"Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?\"\n\n### Update CONTEXT.md inline\n\nWhen a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in `.agents/skills/domain-modeling/CONTEXT-FORMAT.md` (path from the repo root).\n\n`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.\n\n",".agents/skills/ai-chat/SKILL.md":"---\nname: ai-chat\ndescription: Guidance for improving the Windmill AI chat (copilot), especially global mode — tools, prompts, and context-window discipline. Use when editing chat tools, system prompts, or tool-result shapes under frontend/src/lib/components/copilot/chat, or when changing how the chat manages its context window.\n---\n\n## Always benchmark before and after\n\nNo context or behavior change ships without an `ai_evals` A/B on the affected mode.\nAdd or adjust cases for exactly what you changed — see the `ai-evals` skill for\nauthoring and the full run reference.\n\nRun the affected mode **before** your change and **after**, same model(s), same cases.\n\n## Where guidance goes\n\nWhat the model should know about Windmill itself (flow shapes, groups, data tables, app\naccess, secrets…) belongs in `system_prompts/base/` or `system_prompts/languages/`, which also\nfeed the CLI's skills — not in a `*/core.ts` prompt builder, where only this chat would see it.\nThe builders add tool plumbing and runtime values only. Scope a sentence to one consumer with\n`<!-- chat-only -->` / `<!-- cli-only -->`, keep chat tool names out of the shared files, and\nrerun `python system_prompts/generate.py`. `system_prompts/README.md` has the mechanics.\n\n## Measure the window first, and cumulative second\n\nOptimize **`finalContextTokens`** (window occupancy — what drives overflow and\ncompaction), then cumulative prompt tokens.\n\n## Context discipline\n\nThe dominant fixed cost is per-iteration overhead: the system prompt **plus every\ntool schema** is re-sent on every loop iteration. So:\n\n- **Every tool and every parameter is a permanent tax.** Justify each one and measure\n  it; an extra \"locate\" round-trip can cost more than the reads it saves. Strip dead\n  params rather than leaving them in the schema.\n- **Tool results return the minimum.** Never echo content the model already has. The\n  canonical mistake: a write tool that returns the whole edited artifact right after\n  the model authored it — return `{ success, message }` instead. When you touch a\n  *shared* write helper (e.g. `finishAppDraftWrite` in `global/core.ts`), re-check\n  this invariant for **all** the write tools routing through it — the echo has\n  regressed before via a shared refactor.\n\n## Prompts and tool descriptions are part of the surface\n\nThe system prompt and tool descriptions steer behavior as much as the tools\nthemselves, and are benchmarkable the same way. A description that advertises\ntruncation makes the model self-limit; the path-conventions block changes where\ndrafts land. Treat prompt/description edits as real changes and A/B them — a\npure-prompt change is a legitimate, measurable improvement.",".agents/skills/ai-evals/SKILL.md":"---\nname: ai-evals\ndescription: Author and run black-box benchmark cases for the Windmill AI generation modes (flow/app/script/cli/global) in ai_evals/. Use when adding or changing eval cases, or when running before/after benchmarks for AI chat / copilot changes.\n---\n\n# AI evals — authoring and running benchmark cases\n\n`ai_evals/` is a black-box benchmark runner for the Windmill AI generation modes:\n`flow`, `app`, `script`, `cli`, `global`. It always tests the **current** production\nprompts, tools, and guidance in this checkout. Each attempt runs the real production\npath, deterministic validation, then LLM judging.\n\nThe goal is to test current production guidance with realistic user requests — **not**\nto pin one exact implementation shape.\n\n## Running benchmarks\n\n```bash\ncd ai_evals\nbun install                       # first time; frontend modes also need `cd frontend && bun install`\nbun run cli -- models             # list model aliases\nbun run cli -- cases global       # list cases for a mode\nbun run cli -- run global global-test1-script-create --model sonnet\n```\n\nFrontend modes (`flow`/`script`/`app`/`global`) route model calls through a Windmill\nbackend's `/api/w/<ws>/ai/proxy`, so you need **any** reachable backend:\n\n```bash\nWMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:<port> WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests \\\n  bun run cli -- run global <caseIds...> --models sonnet,gpt-5.5,gemini-3.1-pro-preview\n```\n\n- **Reuse an existing workspace.** CE builds cap workspaces, so temp-workspace\n  creation 400s (\"reached workspace limit\"). Always set\n  `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` (or any existing workspace) to\n  reuse one. The only side effect of a run is upserting an `f/evals/ai/<provider>`\n  resource there.\n- Provider keys live in `ai_evals/.env` and are auto-loaded by bun. The judge is a\n  separate Anthropic call (default `claude-sonnet-4-6`) regardless of the model under\n  test.\n\n## Authoring core rules\n\n1. Write prompts like a real user request.\n2. Prefer behavior, inputs, constraints, and outcomes over internal implementation.\n3. Keep deterministic validation narrow and hard.\n4. Put semantic expectations in `judgeChecklist`.\n5. Use `expected` fixtures only when exact structure really matters.\n\n### Prompt writing\n\nPrompts should sound like something a user would naturally ask. Do not write prompts\nas if the user knows Windmill internals unless the case explicitly tests a power-user\nworkflow.\n\nGood:\n- \"Create a flow that routes support requests based on customer tier.\"\n- \"Add a reset button that sets the counter back to 0.\"\n- \"Create a flow that reuses the existing greeting script instead of duplicating the logic.\"\n\nBad:\n- \"Use `branchone` with 3 branches and a default branch.\"\n- \"Create a `rawscript` step with this exact topology.\"\n- \"This is a benchmark harness.\"\n\n### Deterministic validation\n\nUse deterministic checks only for hard failures: missing required files; unexpected\nextra files when the prompt says not to create them; syntax errors; unresolved flow\nrefs; missing required special modules or suspend config; obvious corruption.\n\nDo **not** encode one preferred implementation. Bad hard checks: exact step topology\nfor a creation flow; exact branch structure when the prompt only asked for routing;\nexact input shape when multiple reasonable shapes are acceptable.\n\n### Judge checklist\n\nEvery non-trivial case should have a `judgeChecklist` capturing user-visible behavior\nthat must be present, important constraints, and key completion criteria — not\nlow-level implementation details unless truly required.\n\nGood: \"the flow calculates the order total with 8% tax\"; \"the flow reuses the existing\nworkspace script instead of rewriting the logic\". Bad: \"uses `branchone`\"; \"contains a\n`rawscript` node\".\n\nSee `ai_evals/README.md` for the full case format, fields, and fixture details.",".agents/skills/grill-me/SKILL.md":"---\nname: grill-me\ndescription: A relentless interview to sharpen a plan or design.\ndisable-model-invocation: true\n---\n\nRun a `/grilling` session.\n",".agents/skills/codebase-design/SKILL.md":"---\nname: codebase-design\ndescription: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\n---\n\n# Codebase Design\n\nDesign **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone.\n\n## Glossary\n\nUse these terms exactly — don't substitute \"component,\" \"service,\" \"API,\" or \"boundary.\" Consistent language is the whole point.\n\n**Module** — anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.\n\n**Interface** — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow — they refer only to the type-level surface).\n\n**Implementation** — what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for \"adapter\" when the seam is the topic; \"implementation\" otherwise.\n\n**Depth** — leverage at the interface: the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.\n\n**Seam** _(Michael Feathers)_ — a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).\n\n**Adapter** — a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).\n\n**Leverage** — what callers get from depth: more capability per unit of interface they learn. One implementation pays back across N call sites and M tests.\n\n**Locality** — what maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.\n\n## Deep vs shallow\n\n**Deep module** = small interface + lots of implementation:\n\n```\n┌─────────────────────┐\n│   Small Interface   │  ← Few methods, simple params\n├─────────────────────┤\n│                     │\n│  Deep Implementation│  ← Complex logic hidden\n│                     │\n└─────────────────────┘\n```\n\n**Shallow module** = large interface + little implementation (avoid):\n\n```\n┌─────────────────────────────────┐\n│       Large Interface           │  ← Many methods, complex params\n├─────────────────────────────────┤\n│  Thin Implementation            │  ← Just passes through\n└─────────────────────────────────┘\n```\n\nWhen designing an interface, ask:\n\n- Can I reduce the number of methods?\n- Can I simplify the parameters?\n- Can I hide more complexity inside?\n\n## Principles\n\n- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.\n- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.\n- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.\n- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.\n\n## Designing for testability\n\nGood interfaces make testing natural:\n\n1. **Accept dependencies, don't create them.**\n\n   ```typescript\n   // Testable\n   function processOrder(order, paymentGateway) {}\n\n   // Hard to test\n   function processOrder(order) {\n     const gateway = new StripeGateway();\n   }\n   ```\n\n2. **Return results, don't produce side effects.**\n\n   ```typescript\n   // Testable\n   function calculateDiscount(cart): Discount {}\n\n   // Hard to test\n   function applyDiscount(cart): void {\n     cart.total -= discount;\n   }\n   ```\n\n3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.\n\n## Relationships\n\n- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).\n- **Depth** is a property of a **Module**, measured against its **Interface**.\n- A **Seam** is where a **Module**'s **Interface** lives.\n- An **Adapter** sits at a **Seam** and satisfies the **Interface**.\n- **Depth** produces **Leverage** for callers and **Locality** for maintainers.\n\n## Rejected framings\n\n- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.\n- **\"Interface\" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.\n- **\"Boundary\"**: overloaded with DDD's bounded context. Say **seam** or **interface**.\n\n## Going deeper\n\n- **Deepening a cluster given its dependencies** — see `.agents/skills/codebase-design/DEEPENING.md` (path from the repo root): dependency categories, seam discipline, and replace-don't-layer testing.\n- **Exploring alternative interfaces** — see `.agents/skills/codebase-design/DESIGN-IT-TWICE.md` (path from the repo root): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement.\n",".agents/skills/adding-a-trigger/SKILL.md":"---\nname: adding-a-trigger\ndescription: 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.\n---\n\n# Skill: Adding a New Trigger Type\n\nUse 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.\n\nThe 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.\n\nThroughout this doc, substitute `{kind}` for the new trigger kind (`azure`, `kafka`, …), `{Kind}` for PascalCase (`Azure`, `Kafka`), `{KIND}` for SCREAMING (`AZURE`, `KAFKA`).\n\n## Reference implementations\n\n- **GCP** — closest analogue to Azure. Has push + pull, OIDC auth, ARM-like resource paths, capture handler. Grep for `gcp_trigger` / `GcpTrigger`.\n- **Kafka** — simpler (pull-only, streaming). Good for trivial integrations.\n- **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`.\n\n## 1. Database migration\n\nCreate a migration: `cargo sqlx migrate add -r add_{kind}_trigger` from `backend/`. Never write timestamps manually.\n\nThe `up.sql` usually defines:\n- An optional enum type (e.g. `AZURE_MODE`) if the trigger has sub-kinds\n- The `{kind}_trigger` table with at minimum these columns (mirrored from kafka/gcp):\n  - primary: `(workspace_id, path)`\n  - `script_path`, `is_flow`, `enabled`, `mode`, `permissioned_as`, `edited_by`, `email`\n  - `edited_at`, `error`, `server_id`, `last_server_ping`\n  - `error_handler_path`, `error_handler_args jsonb`, `retry jsonb`\n  - trigger-specific fields\n- Indexes on foreign keys + any frequently-filtered columns\n- Foreign key to `workspace`\n- The RLS policies (`see_own`, `see_member`, `see_folder_extra_perms_user_*`, `see_extra_perms_user_*`, `see_extra_perms_groups_*`), copied from an existing trigger table\n\n**RLS: wrap every session GUC read in a scalar sub-select.** Write the session\nreads as `(select current_setting('session.user'))`,\n`= any((select regexp_split_to_array(current_setting('session.groups'), ','))::text[])`,\n`?| (select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[]`,\n`? (select concat('u/', current_setting('session.user')))`, etc. — not the bare\n`current_setting(...)`. The GUCs are set with `SET LOCAL`, so the sub-select\nhoists them to a one-time InitPlan instead of re-evaluating per scanned row.\nPut the `::text[]` cast **outside** the sub-select for the array cases: in an\n`= any (...)` context, casting inside — `= any((select ...::text[]))` — makes\nPostgres parse the operand as a row-returning subquery and fails at CREATE with\n`operator does not exist: text = text[]`. The outside cast keeps it in\narray-operand form. See migration `20260714230440_wrap_session_gucs_in_rls_policies`\nfor the canonical wrapped forms.\n\nDown migration drops the table and any enum types.\n\n## 2. Backend crate (`windmill-trigger-{kind}`)\n\nCreate a new crate under `backend/windmill-trigger-{kind}/` with:\n\n- `Cargo.toml`: features `enterprise`, `private` if EE, standard deps\n- `src/lib.rs`: `pub use mod_ee::*;` behind `#[cfg(all(feature = \"enterprise\", feature = \"private\"))]`\n- `src/mod_ee.rs`: core types + helpers\n- `src/handler_ee.rs`: `TriggerCrud` impl + route handlers\n- `src/listener_ee.rs`: (only if streaming/pull-based) `Listener` trait impl\n\nRequired in `mod_ee.rs`:\n- `{Kind}Config` struct (persisted shape, `FromRow`)\n- `{Kind}ConfigRequest` struct (what API receives — usually similar to Config but with validation fields)\n- `{Kind}Trigger` unit struct (implements the traits)\n- `impl TriggerJobArgs for {Kind}Trigger` — sets `TRIGGER_KIND`, `Payload`, `v1_payload_fn`\n\nRequired in `handler_ee.rs`:\n- `#[async_trait] impl TriggerCrud for {Kind}Trigger` with:\n  - `type Trigger = Trigger<{Kind}Config>`\n  - `type TriggerConfigRequest = {Kind}ConfigRequest`\n  - `const ROUTE_PREFIX: &'static str = \"/{kind}_triggers\";`\n  - `const TABLE_NAME`, `ADDITIONAL_SELECT_FIELDS`\n  - `get_deployed_object`, `validate_config`, `create_trigger`, `update_trigger`, `delete_trigger`, `test_connection`\n  - `additional_routes` (optional — mount extra endpoints for things like ARM resource listing, topic discovery)\n\nRegister the crate in `backend/Cargo.toml` as a workspace member and as a dep of `windmill-api` behind the feature flag.\n\n## 3. Wire into `windmill-api` (feature-gated everywhere)\n\n**`backend/windmill-api/src/triggers/handler.rs`** — mount the trigger crate:\n```rust\n#[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n{\n    use crate::triggers::{kind}::{Kind}Trigger;\n    router = router.nest({Kind}Trigger::ROUTE_PREFIX, complete_trigger_routes({Kind}Trigger));\n}\n```\n\n**`backend/windmill-api/src/triggers/{kind}/mod.rs`** — re-export the crate:\n```rust\npub use windmill_trigger_{kind}::*;\n```\n\n**`backend/windmill-api/src/lib.rs`** — if the trigger receives inbound pushes, add a webhook route:\n```rust\n.nest(\"/{kind}/w/{workspace_id}\", {\n    #[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n    { triggers::{kind}::handler_oss::{kind}_push_route_handler() }\n    #[cfg(not(...))]\n    { Router::new() }\n})\n```\n\n## 4. `TriggerKind` enum (`backend/windmill-types/src/triggers.rs`)\n\nAlready has slots for most triggers but verify your variant exists:\n- Add `{Kind}` to the `TriggerKind` enum\n- Add match arm in `to_key()`\n- Add match arm in `from_str`\n- Add match arm in `JobTriggerKind` (if jobs need kind tagging)\n\n## 5. OpenAPI (`backend/windmill-api/openapi.yaml`)\n\nThis file is huge and the single most-forgotten place. Add:\n\n- `/w/{workspace}/{kind}_triggers/create` + `/update/{path}` + `/delete/{path}` + `/get/{path}` + `/list` + `/exists/{path}` + `/setmode/{path}` + `/test` paths (mirror gcp section)\n- Any `additional_routes` your handler exposes (resource discovery, etc.)\n- Schemas: `{Kind}Trigger`, `{Kind}TriggerData`, `{Kind}Mode` (if enum), `{Kind}DeliveryConfig`, helper request/response types\n- Add `{kind}` to `CaptureTriggerKind` enum\n- Add `{kind}_used: boolean` to the `UsedTriggers` response schema\n\nRegenerate frontend client: `npm run generate-backend-client` from `frontend/`.\n\n## 6. `UsedTriggers` + workspace export\n\n**`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.\n\n**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON). The block re-uses the `trigger_ignore_keys` variable so the new kind automatically participates in fork-export stripping (`mode` field is omitted when the source workspace is a fork — keeps fork→parent merges from flipping the parent's enabled state).\n\n**Fork cloning (`clone_triggers_and_schedules` in workspaces.rs)** — add an `INSERT INTO {kind}_trigger ... SELECT ...` block that copies all rows from the parent workspace, forcing `mode = 'disabled'::TRIGGER_MODE`. Always runs at fork creation; forgetting this means users can't carry `{kind}` triggers into their forks.\n\n## 6.5 Hardcoded trigger-kind arrays (silent-failure hotspots)\n\nSeveral files keep **hardcoded arrays** of trigger kind strings. Miss one and ACL checks / user offboarding / trash drop your kind:\n\n- **`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.\n- **`backend/windmill-api-users/src/users.rs`** (`extra_perms_tables`) — which tables get `extra_perms` entries cleaned when a user is deleted.\n- **`backend/windmill-api/src/offboarding.rs`** — three separate arrays (enumeration, fork-copy, and delete paths). **All three** need the new kind.\n- **`backend/windmill-api/src/trash.rs`** — `valid_tables` for the trash / restore API.\n- **`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).\n- **`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.\n- **`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.\n\n**OpenAPI enums** to extend (do NOT forget — generated client will allow it but server rejects as 400):\n- `CaptureTriggerKind` enum\n- Three `kind` enums under `/w/{workspace}/acls/{get,add,remove}/{kind}/{path}` (yes, same list repeated three times)\n\nAfter 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.\n\n## 7. Capture infrastructure (`backend/windmill-api/src/capture.rs`)\n\nIf the trigger supports push delivery, it also needs a capture endpoint so users can test it:\n\n- `{Kind}TriggerConfig` struct (gated by feature flags)\n- `TriggerConfig::{Kind}` variant\n- `set_{kind}_trigger_config` function (creates the subscription/equivalent pointing at the capture URL — use your `manage_{kind}_subscription` helper with `trigger_mode=false`)\n- Both real + no-op versions behind feature gates\n- `TriggerKind::{Kind} => set_{kind}_trigger_config(...)` arm in `set_config`\n- `{kind}_payload` async handler — validates auth (if any), processes payload, calls `insert_capture_payload`\n- Route: `.route(\"/{kind}/{runnable_kind}/{*path}\", post({kind}_payload))` inside `workspaced_unauthed_service` — and expand the surrounding `#[cfg(any(...))]` to include your feature flag\n\n## 8. CLI (`cli/`) — easy to miss, breaks sync silently\n\nCheck all of these:\n\n**`cli/src/types.ts`:**\n- Add `\"{kind}\"` to `TRIGGER_TYPES` array\n- Add `\"{kind}_trigger\"` to `getTypeStrFromPath` return union\n- Add match case in `getTypeStrFromPath`'s `typeEnding ===` chain\n- Add `pushTrigger(\"{kind}\", ...)` branch in `pushObj`\n\n**`cli/src/commands/trigger/trigger.ts`:**\n- Import `{Kind}Trigger` type\n- Add `{kind}: {Kind}Trigger` to the `Trigger` type map\n- Add `{kind}: wmill.get{Kind}Trigger`, `update{Kind}Trigger`, `create{Kind}Trigger` to each function map\n- Add `{kind}: { ... }` template to `triggerTemplates`\n- Add `list{Kind}Triggers` call + spread in the `list` aggregation\n- Update `--kind` option descriptions to mention the new kind\n\n**`cli/src/commands/sync/sync.ts`:**\n- Add `path.endsWith(\".{kind}_trigger\" + ext)` in the file-type filter\n- Add `typ == \"{kind}_trigger\"` in `getTypeOrder`\n- Add `\"{kind}_trigger\"` to the delete-suffix regex (~line 3092)\n- Add a `case \"{kind}_trigger\"` in the delete switch\n\n**`cli/src/guidance/skills.ts`** — **DO NOT EDIT DIRECTLY**. It's auto-generated by `system_prompts/generate.py`. Instead:\n- 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)\n- Then run `python3 system_prompts/generate.py` — it regenerates `cli/src/guidance/skills.ts` with the schema extracted from `backend/windmill-api/openapi.yaml`\n- Commit the regenerated file\n\n## 9. Frontend — editor + drawer\n\nUnder `frontend/src/lib/components/triggers/{kind}/`:\n\n- `{Kind}TriggerPanel.svelte` — the tile shown in the triggers listing\n- `{Kind}TriggerEditor.svelte` — outer drawer wrapper\n- `{Kind}TriggerEditorInner.svelte` — state + business logic; must expose:\n  - `openEdit(path, isFlow, defaultValues?)` method\n  - `isEditor` prop, `onConfigChange` + `onCaptureConfigChange` callbacks\n  - `get{Kind}Config()` + `get{Kind}CaptureConfig()` helpers\n  - `captureConfig = $derived.by(untrack(() => isEditor) ? get{Kind}CaptureConfig : () => ({}))`\n  - `$effect(() => { const args = [captureConfig, isValid] as const; untrack(() => onCaptureConfigChange?.(...args)) })`\n- `{Kind}TriggerEditorConfigSection.svelte` — form fields; use design-system components (`TextInput`, `Select`, `Toggle`, `ToggleButtonGroup`), never raw `<input>`\n- `{Kind}Capture.svelte` — capture panel; wraps `CaptureSection` with `captureType=\"{kind}\"`\n- `utils.ts` — `requestBody` builders and any trigger-type-specific helpers\n\n## 10. Frontend — global integration\n\nEasy to miss:\n\n- **`frontend/src/lib/components/triggers.ts`** — add `'{kind}'` to the `TriggerKind` union\n- **`frontend/src/lib/components/triggers/CaptureWrapper.svelte`**:\n  - Import `{Kind}Capture`\n  - Add to `isStreamingCapture()` array (streaming = pull-style; push-style is typically `false`)\n  - Add `{:else if captureType === '{kind}'}` branch with the `<{Kind}Capture>` render\n- **`frontend/src/lib/components/sidebar/SidebarContent.svelte`** — import the icon, add the nav entry\n- **`frontend/src/lib/components/sidebar/OperatorMenu.svelte`** — add the operator-mode entry\n- **`frontend/src/routes/(root)/(logged)/+layout.svelte`** — destructure `{kind}_used` from `/get_used_triggers` response, push `'{kind}'` into `usedKinds`\n- **`frontend/src/lib/components/search/GlobalSearchModal.svelte`** — import icon, add \"Go to {Kind} ...\" entry\n- **`frontend/src/lib/components/offboarding-utils.ts`** — add mappings `{kind}_trigger: '{kind}_triggers'` and `{kind}_trigger: '{kind} trigger'`\n- **`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)\n- **`frontend/src/routes/(root)/(logged)/{kind}_triggers/+page.svelte`** — listing page (mirror `gcp_triggers/+page.svelte` for push+pull, `kafka_triggers` for pure streaming)\n- **`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\n\n## 10.5 AI system prompts (`system_prompts/`)\n\n- **`system_prompts/utils.py`** — append `('{Kind}Trigger', '{kind}_trigger')` to `SCHEMA_MAPPINGS['triggers']` (master list used by code generation + CLI skills)\n- **`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\n- **`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)\n- Run `python3 system_prompts/generate.py` — this rewrites `cli/src/guidance/skills.ts` and all `auto-generated/` docs. Commit the regenerated files\n\n## 11. Validation\n\nRun all of these before declaring done:\n\n```bash\n# Backend\ncd backend\ncargo check --features enterprise,{kind}_trigger,private   # minimal\ncargo check --features enterprise,azure_trigger,private,gcp_trigger,http_trigger,mqtt_trigger,postgres_trigger,sqs_trigger,kafka,nats,smtp,websocket  # full\n\n# SQLx offline data (never run `cargo sqlx prepare` directly — use the wrapper)\n./update_sqlx.sh\n\n# Frontend\ncd frontend\nnpm run generate-backend-client\nnpm run check:fast\n```\n\nSmoke test in the UI: create a trigger, save, check it appears in sidebar + search, delete, re-create via CLI `wmill sync`.\n\n## 12. Common pitfalls\n\n- **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\n- **`.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`\n- **`on:event` directives** — legacy Svelte 4, no-op in runes mode. Use callback props (`onSelected`, `onConfigChange`)\n- **`$bindable(default_value)` on optional props** — banned by project CLAUDE.md. Use `$bindable()` + `$derived(prop ?? default)` instead\n- **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)\n- **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\n- **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\n- **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\n- **Workspace-scoped tag compatibility** — if the trigger has tags, verify forked workspaces handle them (see commit `0773b5bc85` for a historical fix)\n\n## 13. EE file split\n\nIf 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.\n\n## 14. Final checklist before PR\n\n- [ ] Migration up/down tested (revert + re-apply)\n- [ ] `./update_sqlx.sh` committed the updated `.sqlx/` offline data\n- [ ] `cargo check` passes with your feature flag + with all trigger features\n- [ ] `npm run check:fast` passes\n- [ ] Trigger visible in sidebar with correct icon weight (not oversized/colored — use `currentColor`)\n- [ ] Create, edit, delete flow all work in the UI\n- [ ] Capture button works (if push-capable)\n- [ ] Trigger appears in `/get_used_triggers` → sidebar pulse\n- [ ] `wmill sync pull` + `wmill sync push` both round-trip the trigger\n- [ ] `wmill trigger list` includes it\n- [ ] OpenAPI schemas are complete (no `null` in generated types)\n",".agents/skills/local-review/SKILL.md":"---\nname: local-review\ndescription: Code review the current PR (or branch diff against main) for bugs, security, and AGENTS.md compliance. MUST use when asked to review code.\n---\n\n# Local Code Review\n\nRun the same review locally that the GitHub auto-review actions run on PRs (Claude / Codex / Pi). The review policy lives in `REVIEW.md`.\n\n**Why a subagent**: the review MUST run in a fresh context — not inline in the current session. If the user has been iterating on the diff, the main session has absorbed their reasoning and rationalizations, so it anchors and misses things CI catches. A subagent starts cold, like CI does.\n\n## Steps\n\n1. **Determine the PR scope** (cheap, do this in the main session):\n   - If an argument is provided, treat it as a PR number or branch.\n   - Otherwise, detect from the current branch vs `main`.\n   - Confirm the PR/branch exists (`gh pr view <n>` or `git rev-parse <branch>`).\n\n2. **Delegate the review to a fresh-context subagent** with a self-contained prompt. The prompt MUST include:\n   - The PR number or branch name to review.\n   - The instruction to read `REVIEW.md` first for the policy, then `AGENTS.md` files in directories touched by the diff.\n   - The exact output format (see below).\n   - Whether `--comment` was requested (so the subagent emits inline-comment payloads if needed).\n   - Any \"Additional reviewer instructions\" the user provided.\n\n   - **Claude Code**: use the `Agent` tool with `subagent_type: branch-diff-reviewer` (read-only tools, purpose-built for this). If unavailable, fall back to `general-purpose`.\n   - **Codex / Pi**: if the CLI exposes a fresh-session subagent mechanism, use it. Otherwise tell the user to run the skill in a fresh CLI session and stop — running inline in the current session defeats the purpose.\n\n3. **Receive the findings** from the subagent and relay them to the user verbatim. Do not re-summarize, re-judge, or filter — the whole point of fresh context is to surface what the main session would dismiss.\n\n4. **Post comments if `--comment` was requested**: use the `gh` commands below with the subagent's output as the body. The main session does the posting because the subagent is read-only.\n\n## Subagent prompt template\n\n```\nReview <PR #N | branch X> against main per the policy in REVIEW.md.\n\nSteps:\n1. Read REVIEW.md (repo root) for the full policy: severity triage, public-surface\n   checklist, AGENTS.md compliance, test coverage assessment.\n2. Read AGENTS.md (repo root) and any AGENTS.md in directories touched by the diff.\n3. Get the diff: `gh pr diff <N>` (if PR) or `git diff main...<branch>`.\n4. Get context: `gh pr view <N>` (if PR) or `git log main..<branch> --oneline`.\n5. Read changed files only when the diff alone is insufficient to validate a finding.\n6. Self-validate each finding: \"is this definitely a real issue a senior engineer\n   would flag?\" Discard if uncertain.\n7. Output findings in the exact format below. Do not modify any files.\n\n<paste output format from below>\n\n<if --comment requested:>\nAdditionally emit a JSON array of inline comments suitable for the GitHub reviews\nAPI, one per finding that maps to a specific line:\n[{\"path\": \"...\", \"line\": N, \"side\": \"RIGHT\", \"body\": \"[P1] ...\"}, ...]\n```\n\n## Output format\n\n```\n## Code review\n\n<verdict line per REVIEW.md>\n\nFound N issues:\n\n1. [P0|P1|P2] <description>\n   <file_path:line_number>\n\n2. [P0|P1|P2] <description>\n   <file_path:line_number>\n```\n\nEnd with a `Test coverage` section per the shared policy.\n\nIf no issues are found:\n\n```\n## Code review\n\nGood to merge.\n\nNo issues found. Checked for bugs, security, and AGENTS.md compliance.\n```\n\n## Posting comments (`--comment`)\n\nFor a top-level PR comment:\n\n```bash\ngh pr review --comment --body \"<summary from subagent>\"\n```\n\nFor inline comments on specific lines (using the JSON the subagent emitted):\n\n```bash\ngh api repos/{owner}/{repo}/pulls/{pr}/reviews \\\n  -f body=\"<summary>\" -f event=\"COMMENT\" -f comments=\"<json from subagent>\"\n```\n",".agents/skills/improve-codebase-architecture/SKILL.md":"---\nname: improve-codebase-architecture\ndescription: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.\ndisable-model-invocation: true\n---\n\n# Improve Codebase Architecture\n\nSurface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.\n\nThis command is _informed_ by the project's domain model and built on a shared design vocabulary:\n\n- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, \"the interface is the test surface\", \"one adapter = hypothetical seam, two = real\"). Use these terms exactly in every suggestion — don't drift into \"component,\" \"service,\" \"API,\" or \"boundary.\"\n- The domain language in `CONTEXT.md` gives names to good seams.\n\n## Process\n\n### 1. Explore\n\n**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:\n\n- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.\n- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.\n\nRead the project's domain glossary (`CONTEXT.md`) first.\n\nThen spawn a sub-agent to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:\n\n- Where does understanding one concept require bouncing between many small modules?\n- Where are modules **shallow** — interface nearly as complex as the implementation?\n- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?\n- Where do tightly-coupled modules leak across their seams?\n- Which parts of the codebase are untested, or hard to test through their current interface?\n\nApply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A \"yes, concentrates\" is the signal you want.\n\n### 2. Present candidates as an HTML report\n\nWrite a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `<tmpdir>/architecture-review-<timestamp>.html` so each run gets a fresh file. Open it for the user — `xdg-open <path>` on Linux, `open <path>` on macOS, `start <path>` on Windows — and tell them the absolute path.\n\nThe report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.\n\nFor each candidate, render a card with:\n\n- **Files** — which files/modules are involved\n- **Problem** — why the current architecture is causing friction\n- **Solution** — plain English description of what would change\n- **Benefits** — explained in terms of locality and leverage, and how tests would improve\n- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening\n- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge\n\nEnd the report with a **Top recommendation** section: which candidate you'd tackle first and why.\n\n**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines \"Order,\" talk about \"the Order intake module\" — not \"the FooBarHandler,\" and not \"the Order service.\"\n\nSee `.agents/skills/improve-codebase-architecture/HTML-REPORT.md` (path from the repo root) for the full HTML scaffold, diagram patterns, and styling guidance.\n\nDo NOT propose interfaces yet. After the file is written, ask the user: \"Which of these would you like to explore?\"\n\n### 3. Grilling loop\n\nOnce the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.\n\nSide effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:\n\n- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist.\n- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.\n- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.\n",".agents/skills/local-review-codex/SKILL.md":"---\nname: local-review-codex\ndescription: Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy, model and reasoning effort as the codex-pr-review GitHub action.\n---\n\n# Local Codex Review (pre-push)\n\nRuns the exact same review Codex performs in CI (`.github/workflows/codex-pr-review.yml`),\nbut locally and scoped to work you have not pushed yet — so you catch what CI would flag\nbefore the PR exists. Use this before `git push` on a non-trivial change.\n\n**Correspondence with CI** — identical:\n- Policy: `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test coverage).\n- Model: `gpt-6.1-sol`.\n- Reasoning effort: `model_reasoning_effort=\"xhigh\"`.\n- Output: markdown starting with `## Codex Review`, findings tagged P0 / P1 / P2 with file:line.\n\n**Differences from CI** — local-only:\n- Scope is the current branch vs `main` at the merge-base, **including uncommitted changes** (CI reviews a pushed PR diff).\n- Sandbox is `read-only` (CI uses `danger-full-access` on an ephemeral runner). Codex reads the diff and files but cannot modify your working tree.\n- Fresh context is inherent: `codex exec` is a separate cold process, so it does not anchor on the current chat session — the same reason `local-review` insists on a subagent.\n\n## Prerequisites\n\n- `codex` CLI **>= 0.159.3** installed and authed via `codex login` or an `OPENAI_API_KEY` in the environment (which takes priority). Older CLIs reject the model with \"requires a newer version of Codex\"; `run.sh` checks the version up front. Upgrade with `npm install --global @openai/codex@0.159.3` (may need `sudo` for a global install). This matches the pin in `.github/workflows/codex-pr-review.yml` — the CLI version is the same on both sides.\n- `git fetch` the base ref if it's stale, so the merge-base is accurate.\n\n## Run\n\n```bash\nbash .agents/skills/local-review-codex/run.sh          # review vs main (default)\nbash .agents/skills/local-review-codex/run.sh <base>   # review vs a different base ref\n```\n\nInvoke with `bash` (or run the executable directly) — the script needs Bash for\n`set -o pipefail`; `sh` is Dash on Debian/Ubuntu and would fail. If `main` isn't a\nlocal branch (e.g. a fresh single-branch checkout), the runner falls back to\n`origin/main` automatically.\n\nThe script computes `BASE_SHA = git merge-base HEAD <base>`, feeds Codex `REVIEW.md` plus a\ndiff context pointing at `git diff <BASE_SHA>` (which folds in uncommitted edits), and prints\nthe review. It writes only temp files — nothing lands in the working tree.\n\n## Relaying the result\n\nPrint the Codex output verbatim. Do not re-summarize or filter it — the value of a cold Codex\npass is surfacing what the current session would rationalize away. Then decide with the user\nwhether to address findings before pushing.\n\nFor a Claude-native review instead, use `local-review` (branch-diff-reviewer subagent). This\nskill is the Codex counterpart; run both for independent perspectives.\n",".agents/skills/native-trigger/SKILL.md":"---\nname: native-trigger\ndescription: Guidance for adding native trigger services to Windmill. Use when implementing or modifying native trigger integrations across the backend and frontend.\n---\n\n# Skill: Adding Native Trigger Services\n\nThis skill provides comprehensive guidance for adding new native trigger services to Windmill. Native triggers allow external services (like Nextcloud, Google Drive, etc.) to trigger Windmill scripts/flows via webhooks or push notifications.\n\n## Architecture Overview\n\nThe native trigger system consists of:\n\n1. **Database Layer** - PostgreSQL tables and enum types\n2. **Backend Rust Implementation** - Core trait, handlers, and service modules in the `windmill-native-triggers` crate\n3. **Frontend Svelte Components** - Configuration forms and UI components\n\n### Key Files\n\n| Component | Path |\n|-----------|------|\n| Core module with `External` trait | `backend/windmill-native-triggers/src/lib.rs` |\n| Generic CRUD handlers | `backend/windmill-native-triggers/src/handler.rs` |\n| Background sync logic | `backend/windmill-native-triggers/src/sync.rs` |\n| OAuth/workspace integration | `backend/windmill-native-triggers/src/workspace_integrations.rs` |\n| Re-export shim (windmill-api) | `backend/windmill-api/src/native_triggers/mod.rs` |\n| TriggerKind enum | `backend/windmill-common/src/triggers.rs` |\n| JobTriggerKind enum | `backend/windmill-common/src/jobs.rs` |\n| Frontend service registry | `frontend/src/lib/components/triggers/native/utils.ts` |\n| Frontend trigger utilities | `frontend/src/lib/components/triggers/utils.ts` |\n| Trigger badges (icons + counts) | `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte` |\n| Workspace integrations UI | `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte` |\n| OAuth config form component | `frontend/src/lib/components/workspaceSettings/OAuthClientConfig.svelte` |\n| OpenAPI spec | `backend/windmill-api/openapi.yaml` |\n| Reference: Nextcloud module | `backend/windmill-native-triggers/src/nextcloud/` |\n| Reference: Google module | `backend/windmill-native-triggers/src/google/` |\n\n### Crate Structure\n\nThe native trigger code lives in the `windmill-native-triggers` crate (`backend/windmill-native-triggers/`). The `windmill-api` crate re-exports everything via a shim:\n\n```rust\n// backend/windmill-api/src/native_triggers/mod.rs\npub use windmill_native_triggers::*;\n```\n\nAll new service modules go in `backend/windmill-native-triggers/src/`.\n\n---\n\n## Core Concepts\n\n### The `External` Trait\n\nEvery native trigger service implements the `External` trait defined in `lib.rs`:\n\n```rust\n#[async_trait]\npub trait External: Send + Sync + 'static {\n    // Associated types:\n    type ServiceConfig: Debug + DeserializeOwned + Serialize + Send + Sync;\n    type TriggerData: Debug + Serialize + Send + Sync;\n    type OAuthData: DeserializeOwned + Serialize + Clone + Send + Sync;\n    type CreateResponse: DeserializeOwned + Send + Sync;\n\n    // Constants:\n    const SUPPORT_WEBHOOK: bool;\n    const SERVICE_NAME: ServiceName;\n    const DISPLAY_NAME: &'static str;\n    const TOKEN_ENDPOINT: &'static str;\n    const REFRESH_ENDPOINT: &'static str;\n    const AUTH_ENDPOINT: &'static str;\n\n    // Required methods:\n    async fn create(&self, w_id, oauth_data, webhook_token, data, db, tx) -> Result<Self::CreateResponse>;\n    async fn update(&self, w_id, oauth_data, external_id, webhook_token, data, db, tx) -> Result<serde_json::Value>;\n    async fn get(&self, w_id, oauth_data, external_id, db, tx) -> Result<Self::TriggerData>;\n    async fn delete(&self, w_id, oauth_data, external_id, db, tx) -> Result<()>;\n    async fn exists(&self, w_id, oauth_data, external_id, db, tx) -> Result<bool>;\n    async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors);\n    fn external_id_and_metadata_from_response(&self, resp) -> (String, Option<serde_json::Value>);\n\n    // Methods with defaults:\n    async fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned>;\n    fn service_config_from_create_response(&self, data, resp) -> Option<serde_json::Value>;\n    fn additional_routes(&self) -> axum::Router;\n    async fn http_client_request<T, B>(&self, url, method, workspace_id, connection_path, db, headers, body) -> Result<T>;\n}\n```\n\nKey design points:\n- **`update()` returns `serde_json::Value`** - the resolved service_config to store. Each service is responsible for building the final config.\n- **`maintain_triggers()`** - periodic background maintenance. Each service implements its own strategy (Nextcloud: reconcile with external state; Google: renew expiring channels).\n- **No `list_all()` in the trait** - services that need it (Nextcloud) implement it privately; services that don't (Google) use different maintenance strategies.\n- **No `get_external_id_from_trigger_data()` or `extract_service_config_from_trigger_data()`** - removed in favor of the `maintain_triggers` pattern.\n\n### Create Lifecycle: Two Paths\n\nThe `create_native_trigger` handler in `handler.rs` supports two creation flows, controlled by `service_config_from_create_response()`:\n\n**Path A: Short (Google pattern)** - `service_config_from_create_response()` returns `Some(config)`:\n1. `create()` registers on external service\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `service_config_from_create_response()` builds the config directly from input data + response metadata\n4. Stores trigger in DB -- done, no extra round-trip\n\nUse this when the external_id is known before the create call (e.g., Google generates the channel_id as a UUID upfront and includes it in the webhook URL).\n\n**Path B: Long (Nextcloud pattern)** - `service_config_from_create_response()` returns `None` (default):\n1. `create()` registers on external service (webhook URL has no external_id yet)\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `update()` is called to fix the webhook URL with the now-known external_id\n4. `update()` returns the resolved service_config\n5. Stores trigger in DB\n\nUse this when the external_id is assigned by the remote service and the webhook URL needs to be corrected after creation.\n\n### OAuth App and Connections\n\nThe OAuth **app** (client) is configured per workspace and service; **connections** (connected\naccounts) are many per service. Each trigger acts through one connection, named by\n`native_trigger.connection_path`:\n\n| Table | What's Stored |\n|-------|---------------|\n| `workspace_integrations` | `oauth_data` JSON with `base_url`, `client_id`, `client_secret`, `instance_shared` flag. One row per (workspace, service); it must exist for every connection, since token refresh reads the client from it |\n| `variable` | Encrypted `access_token` at the connection path, linked to `account` via its `account` column |\n| `account` | `refresh_token`, with `client` = service name and `is_workspace_integration = true` |\n| `resource` | The connection's resource at the same path, `{ \"token\": \"$var:<path>\" }` |\n\nA user may use a connection when they can read its variable (checked with a user transaction,\nso the variable's row-level security decides): `resolve_usable_connection` /\n`list_usable_connections` in `lib.rs`. The exception is the **workspace connection**: an admin\nmay share one connection per service (`workspace_integrations.resource_path`, set by\n`POST .../workspace_connection`), and every member may then use it without reading its token.\nRenames carry the marker along; disconnecting clears it. Background work (sync, renewal, rename re-registration,\nwebhook handling) uses the stored `connection_path` directly.\n\n`decrypt_oauth_data(db, w_id, service, connection_path)` assembles these into one struct, and\nevery service's `OAuthData` carries `connection_path` so `http_client_request` can refresh and\nwrite back the right connection:\n```rust\npub struct OAuthConfig {\n    pub base_url: String,\n    pub access_token: String,      // decrypted from the connection's variable\n    pub refresh_token: Option<String>, // from its account\n    pub client_id: String,         // from oauth_data or instance settings\n    pub client_secret: String,     // from oauth_data or instance settings\n    pub connection_path: String,\n}\n```\n\nA refreshed token is written back by connection path and account id, never by service: providers\nlike Nextcloud rotate refresh tokens, so writing one onto another account breaks its next refresh.\n\nInstance-level sharing: when the workspace has no OAuth app of its own and the instance admin\nshares one (`share_with_workspaces` in the `oauths` setting; not available for Nextcloud), the\nconnect flow uses it and records `{\"instance_shared\": true}` in `workspace_integrations`.\n\n### URL Resolution\n\nThe `resolve_endpoint()` helper handles both absolute and relative OAuth URLs:\n\n```rust\npub fn resolve_endpoint(base_url: &str, endpoint: &str) -> String {\n    if endpoint.starts_with(\"http://\") || endpoint.starts_with(\"https://\") {\n        endpoint.to_string()  // Google: absolute URLs\n    } else {\n        format!(\"{}{}\", base_url, endpoint)  // Nextcloud: relative paths\n    }\n}\n```\n\n### ServiceName Methods\n\n`ServiceName` is the central registry enum. Each variant must implement these match arms:\n\n| Method | Purpose |\n|--------|---------|\n| `as_str()` | Lowercase identifier (e.g., `\"google\"`) |\n| `as_trigger_kind()` | Maps to `TriggerKind` enum |\n| `as_job_trigger_kind()` | Maps to `JobTriggerKind` enum |\n| `token_endpoint()` | OAuth token endpoint (relative or absolute) |\n| `auth_endpoint()` | OAuth authorization endpoint |\n| `oauth_scopes()` | Space-separated OAuth scopes |\n| `resource_type()` | Resource type for token storage (e.g., `\"gworkspace\"`) |\n| `extra_auth_params()` | Extra OAuth params (e.g., Google needs `access_type=offline`, `prompt=consent`) |\n| `integration_service()` | Maps to the workspace integration service (usually `*self`) |\n| `TryFrom<String>` | Parse from string |\n| `Display` | Delegates to `as_str()` |\n\n---\n\n## Step-by-Step Implementation Guide\n\n### Step 1: Database Migration\n\nCreate a new migration file: `backend/migrations/YYYYMMDDHHMMSS_newservice_trigger.up.sql`\n\n```sql\n-- Add the service to the native_trigger_service enum\nALTER TYPE native_trigger_service ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to TRIGGER_KIND enum (used for trigger tracking)\nALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to job_trigger_kind enum (used for job tracking)\nALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'newservice';\n```\n\nAlso create the corresponding down migration.\n\n### Step 2: Update windmill-common Enums\n\n#### `backend/windmill-common/src/triggers.rs`\n\nAdd variant to `TriggerKind` enum, and update `to_key()` and `fmt()` implementations.\n\n#### `backend/windmill-common/src/jobs.rs`\n\nAdd variant to `JobTriggerKind` enum and update the `Display` implementation.\n\n### Step 3: Backend Service Module\n\nCreate a new directory: `backend/windmill-native-triggers/src/newservice/`\n\n#### `mod.rs` - Type Definitions\n\n```rust\nuse serde::{Deserialize, Serialize};\n\npub mod external;\n// pub mod routes; // Only if you need additional service-specific routes\n\n/// OAuth data deserialized from the three-table pattern.\n/// The actual structure is built by decrypt_oauth_data() from variable + account + workspace_integrations.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct NewServiceOAuthData {\n    pub base_url: String,              // from workspace_integrations.oauth_data\n    pub access_token: String,          // decrypted from variable table\n    pub refresh_token: Option<String>, // from account table\n    // Note: client_id and client_secret are in OAuthConfig, not here\n    // unless the service needs them at runtime for API calls\n}\n\n/// Configuration provided by user when creating/updating a trigger.\n/// Stored as JSON in native_trigger.service_config.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceConfig {\n    // Service-specific configuration fields\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n}\n\n/// Data retrieved from the external service about a trigger.\n/// Returned by the get() method and shown in the UI.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceTriggerData {\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n    // Fields that shouldn't affect service_config comparison should use #[serde(skip_serializing)]\n}\n\n/// Response from external service when creating a trigger/webhook.\n#[derive(Debug, Deserialize)]\npub struct CreateTriggerResponse {\n    pub id: String,\n}\n\n/// Handler struct (stateless, used for routing)\n#[derive(Copy, Clone)]\npub struct NewService;\n```\n\n#### `external.rs` - External Trait Implementation\n\n```rust\nuse async_trait::async_trait;\nuse reqwest::Method;\nuse sqlx::PgConnection;\nuse std::collections::HashMap;\nuse windmill_common::{\n    error::{Error, Result},\n    BASE_URL, DB,\n};\n\nuse crate::{\n    generate_webhook_service_url, External, NativeTrigger, NativeTriggerData, ServiceName,\n    sync::{SyncError, TriggerSyncInfo},\n};\nuse super::{NewService, NewServiceConfig, NewServiceOAuthData, NewServiceTriggerData, CreateTriggerResponse};\n\n#[async_trait]\nimpl External for NewService {\n    type ServiceConfig = NewServiceConfig;\n    type TriggerData = NewServiceTriggerData;\n    type OAuthData = NewServiceOAuthData;\n    type CreateResponse = CreateTriggerResponse;\n\n    const SERVICE_NAME: ServiceName = ServiceName::NewService;\n    const DISPLAY_NAME: &'static str = \"New Service\";\n    const SUPPORT_WEBHOOK: bool = true;\n    const TOKEN_ENDPOINT: &'static str = \"/oauth/token\";\n    const REFRESH_ENDPOINT: &'static str = \"/oauth/token\";\n    const AUTH_ENDPOINT: &'static str = \"/oauth/authorize\";\n\n    async fn create(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::CreateResponse> {\n        let base_url = &*BASE_URL.read().await;\n\n        // external_id is None during create (we get it from the response)\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            None, Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/create\", oauth_data.base_url);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let response: CreateTriggerResponse = self\n            .http_client_request(&url, Method::POST, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        Ok(response)\n    }\n\n    /// Update returns the resolved service_config as JSON.\n    /// For services using the update+get pattern, call self.get() and serialize.\n    async fn update(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<serde_json::Value> {\n        let base_url = &*BASE_URL.read().await;\n\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            Some(external_id), Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let _: serde_json::Value = self\n            .http_client_request(&url, Method::PUT, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        // Fetch back the updated state to get the resolved config\n        let trigger_data = self.get(w_id, oauth_data, external_id, db, tx).await?;\n        serde_json::to_value(&trigger_data)\n            .map_err(|e| Error::InternalErr(format!(\"Failed to serialize trigger data: {}\", e)))\n    }\n\n    async fn get(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::TriggerData> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        self.http_client_request::<_, ()>(&url, Method::GET, w_id, &oauth_data.connection_path, db, None, None).await\n    }\n\n    async fn delete(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<()> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let _: serde_json::Value = self\n            .http_client_request::<_, ()>(&url, Method::DELETE, w_id, &oauth_data.connection_path, db, None, None)\n            .await\n            .or_else(|e| match &e {\n                Error::InternalErr(msg) if msg.contains(\"404\") => Ok(serde_json::Value::Null),\n                _ => Err(e),\n            })?;\n        Ok(())\n    }\n\n    async fn exists(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<bool> {\n        match self.get(w_id, oauth_data, external_id, db, tx).await {\n            Ok(_) => Ok(true),\n            Err(Error::NotFound(_)) => Ok(false),\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Background maintenance. Choose the right pattern for your service:\n    /// - For services with queryable external state: use reconcile_with_external_state()\n    /// - For channel-based services with expiration: implement renewal logic\n    async fn maintain_triggers(\n        &self,\n        db: &DB,\n        workspace_id: &str,\n        triggers: &[NativeTrigger],\n        oauth_data: &Self::OAuthData,\n        synced: &mut Vec<TriggerSyncInfo>,\n        errors: &mut Vec<SyncError>,\n    ) {\n        // Option A: Reconcile with external state (Nextcloud pattern)\n        // Fetch all triggers from external service and compare with DB\n        let external_triggers = match self.list_all(workspace_id, oauth_data, db).await {\n            Ok(triggers) => triggers,\n            Err(e) => {\n                errors.push(SyncError {\n                    resource_path: format!(\"workspace:{}\", workspace_id),\n                    error_message: format!(\"Failed to list triggers: {}\", e),\n                    error_type: \"api_error\".to_string(),\n                });\n                return;\n            }\n        };\n\n        // Convert to (external_id, config_json) pairs\n        let external_pairs: Vec<(String, serde_json::Value)> = external_triggers\n            .into_iter()\n            .map(|t| (t.id.clone(), serde_json::to_value(&t).unwrap_or_default()))\n            .collect();\n\n        crate::sync::reconcile_with_external_state(\n            db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n        ).await;\n    }\n\n    fn external_id_and_metadata_from_response(\n        &self,\n        resp: &Self::CreateResponse,\n    ) -> (String, Option<serde_json::Value>) {\n        (resp.id.clone(), None)\n    }\n\n    // service_config_from_create_response: NOT overridden (returns None).\n    // This means the handler uses the update+get pattern after create.\n    // Override and return Some(...) to skip the update+get cycle (Google pattern).\n}\n\nimpl NewService {\n    /// Private helper to list all triggers from the external service.\n    async fn list_all(\n        &self,\n        w_id: &str,\n        oauth_data: &<Self as External>::OAuthData,\n        db: &DB,\n    ) -> Result<Vec<<Self as External>::TriggerData>> {\n        // Implementation depends on the external service's API\n        todo!()\n    }\n}\n```\n\n### Step 4: Update lib.rs Registry\n\nIn `backend/windmill-native-triggers/src/lib.rs`:\n\n```rust\n// Service modules - add new services here:\n#[cfg(feature = \"native_trigger\")]\npub mod newservice;  // <-- Add this\n\n// ServiceName enum - add variant:\npub enum ServiceName {\n    Nextcloud,\n    Google,\n    NewService,  // <-- Add this\n}\n\n// Then add match arms in ALL ServiceName methods:\n// as_str(), as_trigger_kind(), as_job_trigger_kind(), token_endpoint(),\n// auth_endpoint(), oauth_scopes(), resource_type(), extra_auth_params(),\n// integration_service(), TryFrom<String>, Display\n```\n\n### Step 5: Update handler.rs Routes\n\nIn `backend/windmill-native-triggers/src/handler.rs`:\n\n```rust\npub fn generate_native_trigger_routers() -> Router {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n        return router\n            .nest(\"/nextcloud\", service_routes(NextCloud))\n            .nest(\"/google\", service_routes(Google))\n            .nest(\"/newservice\", service_routes(NewService));  // <-- Add this\n    }\n    // ...\n}\n```\n\n### Step 6: Update sync.rs\n\nIn `backend/windmill-native-triggers/src/sync.rs`:\n\n```rust\npub async fn sync_all_triggers(db: &DB) -> Result<BackgroundSyncResult> {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n\n        // ... existing service syncs ...\n\n        // New service sync\n        let (service_name, result) = sync_service_triggers(db, NewService).await;\n        total_synced += result.synced_triggers.len();\n        total_errors += result.errors.len();\n        service_results.insert(service_name, result);\n    }\n    // ...\n}\n```\n\n### Step 7: Frontend Service Registry\n\nIn `frontend/src/lib/components/triggers/native/utils.ts`:\n\nAdd to `NATIVE_TRIGGER_SERVICES`, `getTriggerIconName()`, and `getServiceIcon()`.\n\n### Step 8: Frontend Trigger Form Component\n\nCreate: `frontend/src/lib/components/triggers/native/services/newservice/NewServiceTriggerForm.svelte`\n\n### Step 9: Frontend Icon Component\n\nCreate: `frontend/src/lib/components/icons/NewServiceIcon.svelte`\n\n### Step 10: Update NativeTriggerEditor\n\nCheck `frontend/src/lib/components/triggers/native/NativeTriggerEditor.svelte` to ensure it dynamically loads form components based on service name.\n\n### Step 11: Workspace Integration UI\n\nAdd your service to the `supportedServices` map in `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte`:\n\n```typescript\nconst supportedServices: Record<string, ServiceConfig> = {\n    // ... existing services ...\n    newservice: {\n        name: 'newservice',\n        displayName: 'New Service',\n        description: 'Connect to New Service for triggers',\n        icon: NewServiceIcon,\n        docsUrl: 'https://www.windmill.dev/docs/integrations/newservice',\n        requiresBaseUrl: false,  // false for cloud services, true for self-hosted\n        setupInstructions: [\n            'Step 1: Create an OAuth app on the service',\n            'Step 2: Configure the redirect URI shown below',\n            'Step 3: Enter the client credentials below'\n        ]\n    }\n}\n```\n\n### Step 12: Update `frontend/src/lib/components/triggers/utils.ts`\n\nUpdate ALL of these maps/functions:\n1. `triggerIconMap` - import and add icon\n2. `triggerDisplayNamesMap` - add display name\n3. `triggerTypeOrder` in `sortTriggers()` - add type\n4. `getLightConfig()` - add case for your service\n5. `getTriggerLabel()` - add case for your service\n6. `jobTriggerKinds` - add to array\n7. `countPropertyMap` - add count property\n8. `triggerSaveFunctions` - add save function\n\n### Step 13: Update TriggersBadge Component\n\nIn `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte`:\n\n1. Import the icon\n2. Add to `baseConfig` with `countKey` (the dynamic `availableNativeServices` loop does NOT set `countKey`)\n3. Add to the `allTypes` array\n\n### Step 14: Update TriggersWrapper.svelte\n\nIn `frontend/src/lib/components/triggers/TriggersWrapper.svelte`:\n\nAdd a `{:else if selectedTrigger.type === 'yourservice'}` case that renders `<NativeTriggersPanel service=\"yourservice\" ...>` with the same props pattern as the existing native trigger cases (e.g., `nextcloud`).\n\n### Step 15: Update AddTriggersButton.svelte\n\nIn `frontend/src/lib/components/triggers/AddTriggersButton.svelte`:\n\n1. Add `yourserviceAvailable` state variable\n2. Add `setYourserviceState()` async function using `isServiceAvailable('yourservice', $workspaceStore!)`\n3. Call it at module level\n4. Add a dropdown entry to `addTriggerItems` with `hidden: !yourserviceAvailable`\n\n### Step 16: Update TriggersEditor.svelte Delete Handling\n\nIn `frontend/src/lib/components/triggers/TriggersEditor.svelte`:\n\nAdd your service to the `nativeTriggerServices` map in `deleteDeployedTrigger()`. Native triggers use `NativeTriggerService.deleteNativeTrigger({ workspace, serviceName, externalId })` instead of the standard `path`-based delete.\n\n### Step 17: Update `getUsedTriggers` for Sidebar Visibility\n\nThe sidebar (`frontend/src/lib/components/sidebar/SidebarContent.svelte`) shows native-trigger links only if `$usedTriggerKinds` includes the service — without this, your trigger page will never appear in the nav bar even when triggers exist.\n\n1. **Backend** — add `{service}_used: bool` to the `UsedTriggers` struct and SELECT in `backend/windmill-api-workspaces/src/workspaces.rs::get_used_triggers()`:\n   ```rust\n   EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = '{service}'::native_trigger_service) AS \"{service}_used!\"\n   ```\n2. **OpenAPI** — add `{service}_used: boolean` to the response schema for `GET /w/{workspace}/workspaces/used_triggers` (under both `properties` and `required`).\n3. **Layout** — in `frontend/src/routes/(root)/(logged)/+layout.svelte::loadUsedTriggerKinds()`, destructure `{service}_used` and push `'{service}'` to `usedKinds`.\n\n### Step 18: Update OpenAPI Spec and Regenerate Types\n\nAdd to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:\n\n```bash\ncd frontend && npm run generate-backend-client\n```\n\n---\n\n## Special Patterns\n\n### Unified Service with `trigger_type` (Google Pattern)\n\nWhen a single service handles multiple trigger types (e.g., Google Drive + Calendar share OAuth and API patterns), use a single `ServiceName` variant with a discriminator field:\n\n```rust\npub enum GoogleTriggerType { Drive, Calendar }\n\npub struct GoogleServiceConfig {\n    pub trigger_type: GoogleTriggerType,\n    // Drive-specific fields (only used when trigger_type = Drive)\n    pub resource_id: Option<String>,\n    pub resource_name: Option<String>,\n    // Calendar-specific fields (only used when trigger_type = Calendar)\n    pub calendar_id: Option<String>,\n    pub calendar_name: Option<String>,\n    // Metadata set after creation\n    pub google_resource_id: Option<String>,\n    pub expiration: Option<String>,\n}\n```\n\nBranch in trait methods based on `trigger_type`. Frontend uses a `ToggleButtonGroup` to switch between types. This keeps the codebase simpler (one service, one OAuth flow, one set of routes).\n\nSee `backend/windmill-native-triggers/src/google/` for the reference implementation.\n\n### Skipping update+get After Create (Google Pattern)\n\nOverride `service_config_from_create_response()` to return `Some(config)` when the external_id is known before the create call:\n\n```rust\nfn service_config_from_create_response(\n    &self,\n    data: &NativeTriggerData<Self::ServiceConfig>,\n    resp: &Self::CreateResponse,\n) -> Option<serde_json::Value> {\n    // Clone input config, add metadata from response\n    let mut config = data.service_config.clone();\n    config.google_resource_id = Some(resp.resource_id.clone());\n    config.expiration = Some(resp.expiration.clone());\n    Some(serde_json::to_value(&config).unwrap())\n}\n```\n\n### Services with Absolute OAuth Endpoints (Google)\n\nUnlike self-hosted services where OAuth endpoints are relative paths appended to `base_url`, services like Google have absolute URLs:\n\n```rust\n// Nextcloud: relative paths\nServiceName::Nextcloud => \"/apps/oauth2/api/v1/token\",\n// Google: absolute URLs\nServiceName::Google => \"https://oauth2.googleapis.com/token\",\n```\n\nThe `resolve_endpoint()` function handles both. For services with absolute endpoints:\n- `base_url` can be empty\n- `requiresBaseUrl: false` in the frontend workspace integration config\n- Add `extra_auth_params()` if needed (Google requires `access_type=offline` and `prompt=consent`)\n\n### Channel-Based Push Notifications with Renewal (Google Pattern)\n\nFor services using expiring watch channels instead of persistent webhooks:\n\n1. Store expiration in `service_config` (as part of `ServiceConfig`)\n2. In `maintain_triggers()`, implement renewal logic instead of using `reconcile_with_external_state()`:\n   ```rust\n   async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors) {\n       for trigger in triggers {\n           if should_renew_channel(trigger) {\n               self.renew_channel(db, trigger, oauth_data).await;\n           }\n       }\n   }\n   ```\n3. Renewal: best-effort stop old channel, create new one with same external_id, update service_config with new expiration\n4. Google example: Drive channels expire in 24h (renew when <1h left), Calendar channels expire in 7 days (renew when <1 day left)\n\n### reconcile_with_external_state (Nextcloud Pattern)\n\nThe reusable function in `sync.rs` compares external triggers with DB state:\n- Triggers missing externally: sets error \"Trigger no longer exists on external service\"\n- Triggers present externally: clears errors, updates service_config if it differs\n\nUsage in `maintain_triggers()`:\n```rust\nlet external_pairs: Vec<(String, serde_json::Value)> = /* fetch from external */;\ncrate::sync::reconcile_with_external_state(\n    db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n).await;\n```\n\n### Webhook Payload Processing\n\nOverride `prepare_webhook()` to parse service-specific payloads into script/flow args:\n\n```rust\nasync fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned> {\n    let mut args = HashMap::new();\n    args.insert(\"event_type\".to_string(), Box::new(headers.get(\"x-event-type\").cloned()) as _);\n    args.insert(\"payload\".to_string(), Box::new(serde_json::from_str::<serde_json::Value>(&body)?) as _);\n    Ok(PushArgsOwned { extra: None, args })\n}\n```\n\nThen register in `prepare_native_trigger_args()` in `lib.rs`:\n```rust\npub async fn prepare_native_trigger_args(service_name, db, w_id, headers, body) -> Result<Option<PushArgsOwned>> {\n    match service_name {\n        ServiceName::Google => { /* ... */ Ok(Some(args)) }\n        ServiceName::NewService => { /* ... */ Ok(Some(args)) }\n        ServiceName::Nextcloud => Ok(None), // Uses default body parsing\n    }\n}\n```\n\n### Instance-Level OAuth Credentials\n\nWhen `workspace_integrations.oauth_data.instance_shared == true`, `decrypt_oauth_data()` reads `client_id` and `client_secret` from instance-level global settings instead of workspace-level. This allows admins to share OAuth app credentials across workspaces.\n\n`generate_connect_url` picks the client (`resolve_oauth_client`): the workspace's own app when configured, the instance's otherwise. A GitHub OAuth app only redirects below its single registered callback, so connections made with the instance GitHub app land on `/oauth/callback/github/native_trigger` instead of the workspace settings page.\n\n---\n\n## Testing Checklist\n\n- [ ] Database migration runs successfully\n- [ ] `cargo check -p windmill-native-triggers --features native_trigger` passes\n- [ ] `npx svelte-check --threshold error` passes (in frontend/)\n- [ ] Service appears in workspace integrations list\n- [ ] OAuth flow completes successfully\n- [ ] Can create a new trigger\n- [ ] Can view trigger details\n- [ ] Can update trigger configuration\n- [ ] Can delete trigger\n- [ ] Webhook receives and processes payloads\n- [ ] Background sync works correctly (reconciliation or channel renewal)\n- [ ] Error handling works (expired tokens, service unavailable)\n\n---\n\n## Reference Implementations\n\n### Nextcloud (Self-Hosted, Update+Get Pattern)\n\n| File | Purpose |\n|------|---------|\n| `nextcloud/mod.rs` | Types: NextCloudOAuthData, NextcloudServiceConfig, NextCloudTriggerData |\n| `nextcloud/external.rs` | External trait: uses update+get pattern, reconcile_with_external_state for sync |\n| `nextcloud/routes.rs` | Additional route: `GET /events` |\n\nKey patterns: relative OAuth endpoints, base_url required, list_all + reconcile for sync, update returns JSON from get().\n\n### Google (Cloud, Unified Service, Short Create)\n\n| File | Purpose |\n|------|---------|\n| `google/mod.rs` | Types: GoogleServiceConfig with trigger_type discriminator, GoogleTriggerType enum |\n| `google/external.rs` | External trait: overrides service_config_from_create_response, channel renewal for sync |\n| `google/routes.rs` | Additional routes: `GET /calendars`, `GET /drive/files`, `GET /drive/shared_drives` |\n\nKey patterns: absolute OAuth endpoints, empty base_url, trigger_type for Drive/Calendar, expiring watch channels with renewal, service_config_from_create_response skips update+get, get() reconstructs data from stored service_config (no external \"get channel\" API).\n",".agents/skills/grilling/SKILL.md":"---\nname: grilling\ndescription: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n---\n\nInterview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.\n\nWork the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.\n\nEach question should be formatted like so:\n\n```\n❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>\n\n➡️ <your recommended answer>\n```\n\nEach round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.\n\nFinding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it — don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report — ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait.\n\nThe session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.\n",".agents/skills/pr/SKILL.md":"# pr\n\nPath: `.agents/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/pr/SKILL.md)",".agents/skills/refine/SKILL.md":"# refine\n\nPath: `.agents/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/refine/SKILL.md)",".agents/skills/rust-backend/SKILL.md":"# rust-backend\n\nPath: `.agents/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/rust-backend/SKILL.md)",".agents/skills/svelte-frontend/SKILL.md":"# svelte-frontend\n\nPath: `.agents/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/svelte-frontend/SKILL.md)",".agents/skills/update-sqlx/SKILL.md":"# update-sqlx\n\nPath: `.agents/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/update-sqlx/SKILL.md)",".claude/skills/adding-a-trigger/SKILL.md":"# adding-a-trigger\n\nPath: `.claude/skills/adding-a-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/adding-a-trigger/SKILL.md)",".claude/skills/ai-chat/SKILL.md":"# ai-chat\n\nPath: `.claude/skills/ai-chat/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-chat/SKILL.md)",".claude/skills/ai-evals/SKILL.md":"# ai-evals\n\nPath: `.claude/skills/ai-evals/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-evals/SKILL.md)",".claude/skills/codebase-design/SKILL.md":"# codebase-design\n\nPath: `.claude/skills/codebase-design/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/codebase-design/SKILL.md)",".claude/skills/commit/SKILL.md":"# commit\n\nPath: `.claude/skills/commit/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/commit/SKILL.md)",".claude/skills/domain-modeling/SKILL.md":"# domain-modeling\n\nPath: `.claude/skills/domain-modeling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/domain-modeling/SKILL.md)",".claude/skills/grill-me/SKILL.md":"# grill-me\n\nPath: `.claude/skills/grill-me/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grill-me/SKILL.md)",".claude/skills/grilling/SKILL.md":"# grilling\n\nPath: `.claude/skills/grilling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grilling/SKILL.md)",".claude/skills/improve-codebase-architecture/SKILL.md":"# improve-codebase-architecture\n\nPath: `.claude/skills/improve-codebase-architecture/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/improve-codebase-architecture/SKILL.md)",".claude/skills/local-review-codex/SKILL.md":"# local-review-codex\n\nPath: `.claude/skills/local-review-codex/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review-codex/SKILL.md)",".claude/skills/local-review/SKILL.md":"# local-review\n\nPath: `.claude/skills/local-review/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review/SKILL.md)",".claude/skills/native-trigger/SKILL.md":"# native-trigger\n\nPath: `.claude/skills/native-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/native-trigger/SKILL.md)",".claude/skills/pr/SKILL.md":"# pr\n\nPath: `.claude/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/pr/SKILL.md)",".claude/skills/refine/SKILL.md":"# refine\n\nPath: `.claude/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/refine/SKILL.md)",".claude/skills/rust-backend/SKILL.md":"# rust-backend\n\nPath: `.claude/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/rust-backend/SKILL.md)",".claude/skills/svelte-frontend/SKILL.md":"# svelte-frontend\n\nPath: `.claude/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/svelte-frontend/SKILL.md)",".claude/skills/update-sqlx/SKILL.md":"# update-sqlx\n\nPath: `.claude/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/update-sqlx/SKILL.md)","system_prompts/auto-generated/skills/cli-commands/SKILL.md":"# cli-commands\n\nPath: `system_prompts/auto-generated/skills/cli-commands/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/cli-commands/SKILL.md)","system_prompts/auto-generated/skills/datatables/SKILL.md":"# datatables\n\nPath: `system_prompts/auto-generated/skills/datatables/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/datatables/SKILL.md)","system_prompts/auto-generated/skills/preview/SKILL.md":"# preview\n\nPath: `system_prompts/auto-generated/skills/preview/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/preview/SKILL.md)","system_prompts/auto-generated/skills/raw-app/SKILL.md":"# raw-app\n\nPath: `system_prompts/auto-generated/skills/raw-app/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/raw-app/SKILL.md)","system_prompts/auto-generated/skills/resources/SKILL.md":"# resources\n\nPath: `system_prompts/auto-generated/skills/resources/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/resources/SKILL.md)","system_prompts/auto-generated/skills/schedules/SKILL.md":"# schedules\n\nPath: `system_prompts/auto-generated/skills/schedules/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/schedules/SKILL.md)","system_prompts/auto-generated/skills/triggers/SKILL.md":"# triggers\n\nPath: `system_prompts/auto-generated/skills/triggers/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/triggers/SKILL.md)","system_prompts/auto-generated/skills/workspace-settings/SKILL.md":"# workspace-settings\n\nPath: `system_prompts/auto-generated/skills/workspace-settings/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/workspace-settings/SKILL.md)","system_prompts/auto-generated/skills/write-flow/SKILL.md":"# write-flow\n\nPath: `system_prompts/auto-generated/skills/write-flow/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-flow/SKILL.md)","system_prompts/auto-generated/skills/write-pipeline/SKILL.md":"# write-pipeline\n\nPath: `system_prompts/auto-generated/skills/write-pipeline/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-pipeline/SKILL.md)","system_prompts/auto-generated/skills/write-script-ansible/SKILL.md":"# write-script-ansible\n\nPath: `system_prompts/auto-generated/skills/write-script-ansible/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-ansible/SKILL.md)","system_prompts/auto-generated/skills/write-script-bash/SKILL.md":"# write-script-bash\n\nPath: `system_prompts/auto-generated/skills/write-script-bash/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bash/SKILL.md)","system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md":"# write-script-bigquery\n\nPath: `system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md)","system_prompts/auto-generated/skills/write-script-bun/SKILL.md":"# write-script-bun\n\nPath: `system_prompts/auto-generated/skills/write-script-bun/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bun/SKILL.md)","system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md":"# write-script-bunnative\n\nPath: `system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md)","system_prompts/auto-generated/skills/write-script-csharp/SKILL.md":"# write-script-csharp\n\nPath: `system_prompts/auto-generated/skills/write-script-csharp/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md)","system_prompts/auto-generated/skills/write-script-deno/SKILL.md":"# write-script-deno\n\nPath: `system_prompts/auto-generated/skills/write-script-deno/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-deno/SKILL.md)","system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md":"# write-script-duckdb\n\nPath: `system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md)","system_prompts/auto-generated/skills/write-script-go/SKILL.md":"# write-script-go\n\nPath: `system_prompts/auto-generated/skills/write-script-go/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-go/SKILL.md)","system_prompts/auto-generated/skills/write-script-graphql/SKILL.md":"# write-script-graphql\n\nPath: `system_prompts/auto-generated/skills/write-script-graphql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md)","system_prompts/auto-generated/skills/write-script-java/SKILL.md":"# write-script-java\n\nPath: `system_prompts/auto-generated/skills/write-script-java/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-java/SKILL.md)","system_prompts/auto-generated/skills/write-script-mssql/SKILL.md":"# write-script-mssql\n\nPath: `system_prompts/auto-generated/skills/write-script-mssql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md)","system_prompts/auto-generated/skills/write-script-mysql/SKILL.md":"# write-script-mysql\n\nPath: `system_prompts/auto-generated/skills/write-script-mysql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md)","system_prompts/auto-generated/skills/write-script-php/SKILL.md":"# write-script-php\n\nPath: `system_prompts/auto-generated/skills/write-script-php/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-php/SKILL.md)","system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md":"# write-script-postgresql\n\nPath: `system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md)","system_prompts/auto-generated/skills/write-script-powershell/SKILL.md":"# write-script-powershell\n\nPath: `system_prompts/auto-generated/skills/write-script-powershell/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md)","system_prompts/auto-generated/skills/write-script-python3/SKILL.md":"# write-script-python3\n\nPath: `system_prompts/auto-generated/skills/write-script-python3/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-python3/SKILL.md)","system_prompts/auto-generated/skills/write-script-rlang/SKILL.md":"# write-script-rlang\n\nPath: `system_prompts/auto-generated/skills/write-script-rlang/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md)","system_prompts/auto-generated/skills/write-script-rust/SKILL.md":"# write-script-rust\n\nPath: `system_prompts/auto-generated/skills/write-script-rust/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rust/SKILL.md)","system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md":"# write-script-snowflake\n\nPath: `system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md)","system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md":"# write-workflow-as-code\n\nPath: `system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/CLAUDE.md)",".codex/config.toml":"# Codex Directive: config.toml\n\nPath: `.codex/config.toml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.codex/config.toml)","ai_evals/AGENTS.md":"# ai_evals - AI Agent Protocol & Instructions\n\nPath: `ai_evals/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/AGENTS.md)","ai_evals/CLAUDE.md":"# ai_evals - Claude Agent Guidelines & System Prompt\n\nPath: `ai_evals/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/CLAUDE.md)","backend/AGENTS.md":"# backend - AI Agent Protocol & Instructions\n\nPath: `backend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/AGENTS.md)","backend/CLAUDE.md":"# backend - Claude Agent Guidelines & System Prompt\n\nPath: `backend/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/CLAUDE.md)","frontend/AGENTS.md":"# frontend - AI Agent Protocol & Instructions\n\nPath: `frontend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/AGENTS.md)","frontend/CLAUDE.md":"# frontend - Claude Agent Guidelines & System Prompt\n\nPath: `frontend/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/CLAUDE.md)",".agents/skills/UPSTREAM.md":"# Skills Skill\n\nPath: `.agents/skills/UPSTREAM.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/UPSTREAM.md)",".claude/agents/branch-diff-reviewer.md":"# Subagent: branch-diff-reviewer\n\nPath: `.claude/agents/branch-diff-reviewer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/branch-diff-reviewer.md)",".claude/agents/openapi-sync.md":"# Subagent: openapi-sync\n\nPath: `.claude/agents/openapi-sync.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/openapi-sync.md)","backend/generate_mcp_endpoints_tools/README.md":"# generate_mcp_endpoints_tools Documentation\n\nPath: `backend/generate_mcp_endpoints_tools/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/generate_mcp_endpoints_tools/README.md)",".agents/skills/codebase-design/DEEPENING.md":"# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DEEPENING.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DEEPENING.md)",".agents/skills/codebase-design/DESIGN-IT-TWICE.md":"# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DESIGN-IT-TWICE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DESIGN-IT-TWICE.md)",".agents/skills/domain-modeling/CONTEXT-FORMAT.md":"# Domain-modeling Skill\n\nPath: `.agents/skills/domain-modeling/CONTEXT-FORMAT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/domain-modeling/CONTEXT-FORMAT.md)",".agents/skills/improve-codebase-architecture/HTML-REPORT.md":"# Improve-codebase-architecture Skill\n\nPath: `.agents/skills/improve-codebase-architecture/HTML-REPORT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/improve-codebase-architecture/HTML-REPORT.md)","ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml)","ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml)","ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml":"# Reuse_greeting__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml)","ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml)"},"files":{".agents/skills/commit/SKILL.md":"---\nname: commit\nuser_invocable: true\ndescription: Create a git commit with conventional commit format. MUST use anytime you want to commit changes.\n---\n\n# Git Commit Skill\n\nCreate a focused, single-line commit following conventional commit conventions.\n\n## Instructions\n\n1. **Analyze changes**: Run `git status` and `git diff` to understand what was modified\n2. **Stage only modified files**: Add files individually by name. NEVER use `git add -A` or `git add .`\n3. **Write commit message**: Follow the conventional commit format as a single line\n\n## Conventional Commit Format\n\n```\n<type>: <description>\n```\n\n### Types\n- `feat`: New feature or capability\n- `fix`: Bug fix\n- `refactor`: Code change that neither fixes a bug nor adds a feature\n- `docs`: Documentation only changes\n- `style`: Formatting, missing semicolons, etc (no code change)\n- `test`: Adding or correcting tests\n- `chore`: Maintenance tasks, dependency updates, etc\n- `perf`: Performance improvement\n\n### Rules\n- Message MUST be a single line (no multi-line messages)\n- Description should be lowercase, imperative mood (\"add\" not \"added\")\n- No period at the end\n- Keep under 72 characters total\n\n### Examples\n```\nfeat: add token usage tracking for AI providers\nfix: resolve null pointer in job executor\nrefactor: extract common validation logic\ndocs: update API endpoint documentation\nchore: upgrade sqlx to 0.7\n```\n\n## Execution Steps\n\n1. Run `git status` to see all changes\n2. Run `git diff` to understand the changes in detail\n3. Run `git log --oneline -5` to see recent commit style\n4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`\n5. Create the commit with conventional format:\n   ```bash\n   git commit -m \"<type>: <description>\"\n   ```\n6. Run `git status` to verify the commit succeeded\n",".agents/skills/domain-modeling/SKILL.md":"---\nname: domain-modeling\ndescription: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, or when another skill needs to maintain the domain model.\n---\n\n# Domain Modeling\n\nActively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)\n\n## File structure\n\nMost repos have a single context:\n\n```\n/\n├── CONTEXT.md\n└── src/\n```\n\nIf a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:\n\n```\n/\n├── CONTEXT-MAP.md\n└── src/\n    ├── ordering/\n    │   └── CONTEXT.md\n    └── billing/\n        └── CONTEXT.md\n```\n\nCreate files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved.\n\n## During the session\n\n### Challenge against the glossary\n\nWhen the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. \"Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?\"\n\n### Sharpen fuzzy language\n\nWhen the user uses vague or overloaded terms, propose a precise canonical term. \"You're saying 'account' — do you mean the Customer or the User? Those are different things.\"\n\n### Discuss concrete scenarios\n\nWhen domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.\n\n### Cross-reference with code\n\nWhen the user states how something works, check whether the code agrees. If you find a contradiction, surface it: \"Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?\"\n\n### Update CONTEXT.md inline\n\nWhen a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in `.agents/skills/domain-modeling/CONTEXT-FORMAT.md` (path from the repo root).\n\n`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.\n\n",".agents/skills/ai-chat/SKILL.md":"---\nname: ai-chat\ndescription: Guidance for improving the Windmill AI chat (copilot), especially global mode — tools, prompts, and context-window discipline. Use when editing chat tools, system prompts, or tool-result shapes under frontend/src/lib/components/copilot/chat, or when changing how the chat manages its context window.\n---\n\n## Always benchmark before and after\n\nNo context or behavior change ships without an `ai_evals` A/B on the affected mode.\nAdd or adjust cases for exactly what you changed — see the `ai-evals` skill for\nauthoring and the full run reference.\n\nRun the affected mode **before** your change and **after**, same model(s), same cases.\n\n## Where guidance goes\n\nWhat the model should know about Windmill itself (flow shapes, groups, data tables, app\naccess, secrets…) belongs in `system_prompts/base/` or `system_prompts/languages/`, which also\nfeed the CLI's skills — not in a `*/core.ts` prompt builder, where only this chat would see it.\nThe builders add tool plumbing and runtime values only. Scope a sentence to one consumer with\n`<!-- chat-only -->` / `<!-- cli-only -->`, keep chat tool names out of the shared files, and\nrerun `python system_prompts/generate.py`. `system_prompts/README.md` has the mechanics.\n\n## Measure the window first, and cumulative second\n\nOptimize **`finalContextTokens`** (window occupancy — what drives overflow and\ncompaction), then cumulative prompt tokens.\n\n## Context discipline\n\nThe dominant fixed cost is per-iteration overhead: the system prompt **plus every\ntool schema** is re-sent on every loop iteration. So:\n\n- **Every tool and every parameter is a permanent tax.** Justify each one and measure\n  it; an extra \"locate\" round-trip can cost more than the reads it saves. Strip dead\n  params rather than leaving them in the schema.\n- **Tool results return the minimum.** Never echo content the model already has. The\n  canonical mistake: a write tool that returns the whole edited artifact right after\n  the model authored it — return `{ success, message }` instead. When you touch a\n  *shared* write helper (e.g. `finishAppDraftWrite` in `global/core.ts`), re-check\n  this invariant for **all** the write tools routing through it — the echo has\n  regressed before via a shared refactor.\n\n## Prompts and tool descriptions are part of the surface\n\nThe system prompt and tool descriptions steer behavior as much as the tools\nthemselves, and are benchmarkable the same way. A description that advertises\ntruncation makes the model self-limit; the path-conventions block changes where\ndrafts land. Treat prompt/description edits as real changes and A/B them — a\npure-prompt change is a legitimate, measurable improvement.",".agents/skills/ai-evals/SKILL.md":"---\nname: ai-evals\ndescription: Author and run black-box benchmark cases for the Windmill AI generation modes (flow/app/script/cli/global) in ai_evals/. Use when adding or changing eval cases, or when running before/after benchmarks for AI chat / copilot changes.\n---\n\n# AI evals — authoring and running benchmark cases\n\n`ai_evals/` is a black-box benchmark runner for the Windmill AI generation modes:\n`flow`, `app`, `script`, `cli`, `global`. It always tests the **current** production\nprompts, tools, and guidance in this checkout. Each attempt runs the real production\npath, deterministic validation, then LLM judging.\n\nThe goal is to test current production guidance with realistic user requests — **not**\nto pin one exact implementation shape.\n\n## Running benchmarks\n\n```bash\ncd ai_evals\nbun install                       # first time; frontend modes also need `cd frontend && bun install`\nbun run cli -- models             # list model aliases\nbun run cli -- cases global       # list cases for a mode\nbun run cli -- run global global-test1-script-create --model sonnet\n```\n\nFrontend modes (`flow`/`script`/`app`/`global`) route model calls through a Windmill\nbackend's `/api/w/<ws>/ai/proxy`, so you need **any** reachable backend:\n\n```bash\nWMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:<port> WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests \\\n  bun run cli -- run global <caseIds...> --models sonnet,gpt-5.5,gemini-3.1-pro-preview\n```\n\n- **Reuse an existing workspace.** CE builds cap workspaces, so temp-workspace\n  creation 400s (\"reached workspace limit\"). Always set\n  `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` (or any existing workspace) to\n  reuse one. The only side effect of a run is upserting an `f/evals/ai/<provider>`\n  resource there.\n- Provider keys live in `ai_evals/.env` and are auto-loaded by bun. The judge is a\n  separate Anthropic call (default `claude-sonnet-4-6`) regardless of the model under\n  test.\n\n## Authoring core rules\n\n1. Write prompts like a real user request.\n2. Prefer behavior, inputs, constraints, and outcomes over internal implementation.\n3. Keep deterministic validation narrow and hard.\n4. Put semantic expectations in `judgeChecklist`.\n5. Use `expected` fixtures only when exact structure really matters.\n\n### Prompt writing\n\nPrompts should sound like something a user would naturally ask. Do not write prompts\nas if the user knows Windmill internals unless the case explicitly tests a power-user\nworkflow.\n\nGood:\n- \"Create a flow that routes support requests based on customer tier.\"\n- \"Add a reset button that sets the counter back to 0.\"\n- \"Create a flow that reuses the existing greeting script instead of duplicating the logic.\"\n\nBad:\n- \"Use `branchone` with 3 branches and a default branch.\"\n- \"Create a `rawscript` step with this exact topology.\"\n- \"This is a benchmark harness.\"\n\n### Deterministic validation\n\nUse deterministic checks only for hard failures: missing required files; unexpected\nextra files when the prompt says not to create them; syntax errors; unresolved flow\nrefs; missing required special modules or suspend config; obvious corruption.\n\nDo **not** encode one preferred implementation. Bad hard checks: exact step topology\nfor a creation flow; exact branch structure when the prompt only asked for routing;\nexact input shape when multiple reasonable shapes are acceptable.\n\n### Judge checklist\n\nEvery non-trivial case should have a `judgeChecklist` capturing user-visible behavior\nthat must be present, important constraints, and key completion criteria — not\nlow-level implementation details unless truly required.\n\nGood: \"the flow calculates the order total with 8% tax\"; \"the flow reuses the existing\nworkspace script instead of rewriting the logic\". Bad: \"uses `branchone`\"; \"contains a\n`rawscript` node\".\n\nSee `ai_evals/README.md` for the full case format, fields, and fixture details.",".agents/skills/grill-me/SKILL.md":"---\nname: grill-me\ndescription: A relentless interview to sharpen a plan or design.\ndisable-model-invocation: true\n---\n\nRun a `/grilling` session.\n",".agents/skills/codebase-design/SKILL.md":"---\nname: codebase-design\ndescription: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\n---\n\n# Codebase Design\n\nDesign **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone.\n\n## Glossary\n\nUse these terms exactly — don't substitute \"component,\" \"service,\" \"API,\" or \"boundary.\" Consistent language is the whole point.\n\n**Module** — anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.\n\n**Interface** — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow — they refer only to the type-level surface).\n\n**Implementation** — what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for \"adapter\" when the seam is the topic; \"implementation\" otherwise.\n\n**Depth** — leverage at the interface: the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.\n\n**Seam** _(Michael Feathers)_ — a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).\n\n**Adapter** — a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).\n\n**Leverage** — what callers get from depth: more capability per unit of interface they learn. One implementation pays back across N call sites and M tests.\n\n**Locality** — what maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.\n\n## Deep vs shallow\n\n**Deep module** = small interface + lots of implementation:\n\n```\n┌─────────────────────┐\n│   Small Interface   │  ← Few methods, simple params\n├─────────────────────┤\n│                     │\n│  Deep Implementation│  ← Complex logic hidden\n│                     │\n└─────────────────────┘\n```\n\n**Shallow module** = large interface + little implementation (avoid):\n\n```\n┌─────────────────────────────────┐\n│       Large Interface           │  ← Many methods, complex params\n├─────────────────────────────────┤\n│  Thin Implementation            │  ← Just passes through\n└─────────────────────────────────┘\n```\n\nWhen designing an interface, ask:\n\n- Can I reduce the number of methods?\n- Can I simplify the parameters?\n- Can I hide more complexity inside?\n\n## Principles\n\n- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.\n- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.\n- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.\n- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.\n\n## Designing for testability\n\nGood interfaces make testing natural:\n\n1. **Accept dependencies, don't create them.**\n\n   ```typescript\n   // Testable\n   function processOrder(order, paymentGateway) {}\n\n   // Hard to test\n   function processOrder(order) {\n     const gateway = new StripeGateway();\n   }\n   ```\n\n2. **Return results, don't produce side effects.**\n\n   ```typescript\n   // Testable\n   function calculateDiscount(cart): Discount {}\n\n   // Hard to test\n   function applyDiscount(cart): void {\n     cart.total -= discount;\n   }\n   ```\n\n3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.\n\n## Relationships\n\n- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).\n- **Depth** is a property of a **Module**, measured against its **Interface**.\n- A **Seam** is where a **Module**'s **Interface** lives.\n- An **Adapter** sits at a **Seam** and satisfies the **Interface**.\n- **Depth** produces **Leverage** for callers and **Locality** for maintainers.\n\n## Rejected framings\n\n- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.\n- **\"Interface\" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.\n- **\"Boundary\"**: overloaded with DDD's bounded context. Say **seam** or **interface**.\n\n## Going deeper\n\n- **Deepening a cluster given its dependencies** — see `.agents/skills/codebase-design/DEEPENING.md` (path from the repo root): dependency categories, seam discipline, and replace-don't-layer testing.\n- **Exploring alternative interfaces** — see `.agents/skills/codebase-design/DESIGN-IT-TWICE.md` (path from the repo root): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement.\n",".agents/skills/adding-a-trigger/SKILL.md":"---\nname: adding-a-trigger\ndescription: 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.\n---\n\n# Skill: Adding a New Trigger Type\n\nUse 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.\n\nThe 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.\n\nThroughout this doc, substitute `{kind}` for the new trigger kind (`azure`, `kafka`, …), `{Kind}` for PascalCase (`Azure`, `Kafka`), `{KIND}` for SCREAMING (`AZURE`, `KAFKA`).\n\n## Reference implementations\n\n- **GCP** — closest analogue to Azure. Has push + pull, OIDC auth, ARM-like resource paths, capture handler. Grep for `gcp_trigger` / `GcpTrigger`.\n- **Kafka** — simpler (pull-only, streaming). Good for trivial integrations.\n- **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`.\n\n## 1. Database migration\n\nCreate a migration: `cargo sqlx migrate add -r add_{kind}_trigger` from `backend/`. Never write timestamps manually.\n\nThe `up.sql` usually defines:\n- An optional enum type (e.g. `AZURE_MODE`) if the trigger has sub-kinds\n- The `{kind}_trigger` table with at minimum these columns (mirrored from kafka/gcp):\n  - primary: `(workspace_id, path)`\n  - `script_path`, `is_flow`, `enabled`, `mode`, `permissioned_as`, `edited_by`, `email`\n  - `edited_at`, `error`, `server_id`, `last_server_ping`\n  - `error_handler_path`, `error_handler_args jsonb`, `retry jsonb`\n  - trigger-specific fields\n- Indexes on foreign keys + any frequently-filtered columns\n- Foreign key to `workspace`\n- The RLS policies (`see_own`, `see_member`, `see_folder_extra_perms_user_*`, `see_extra_perms_user_*`, `see_extra_perms_groups_*`), copied from an existing trigger table\n\n**RLS: wrap every session GUC read in a scalar sub-select.** Write the session\nreads as `(select current_setting('session.user'))`,\n`= any((select regexp_split_to_array(current_setting('session.groups'), ','))::text[])`,\n`?| (select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[]`,\n`? (select concat('u/', current_setting('session.user')))`, etc. — not the bare\n`current_setting(...)`. The GUCs are set with `SET LOCAL`, so the sub-select\nhoists them to a one-time InitPlan instead of re-evaluating per scanned row.\nPut the `::text[]` cast **outside** the sub-select for the array cases: in an\n`= any (...)` context, casting inside — `= any((select ...::text[]))` — makes\nPostgres parse the operand as a row-returning subquery and fails at CREATE with\n`operator does not exist: text = text[]`. The outside cast keeps it in\narray-operand form. See migration `20260714230440_wrap_session_gucs_in_rls_policies`\nfor the canonical wrapped forms.\n\nDown migration drops the table and any enum types.\n\n## 2. Backend crate (`windmill-trigger-{kind}`)\n\nCreate a new crate under `backend/windmill-trigger-{kind}/` with:\n\n- `Cargo.toml`: features `enterprise`, `private` if EE, standard deps\n- `src/lib.rs`: `pub use mod_ee::*;` behind `#[cfg(all(feature = \"enterprise\", feature = \"private\"))]`\n- `src/mod_ee.rs`: core types + helpers\n- `src/handler_ee.rs`: `TriggerCrud` impl + route handlers\n- `src/listener_ee.rs`: (only if streaming/pull-based) `Listener` trait impl\n\nRequired in `mod_ee.rs`:\n- `{Kind}Config` struct (persisted shape, `FromRow`)\n- `{Kind}ConfigRequest` struct (what API receives — usually similar to Config but with validation fields)\n- `{Kind}Trigger` unit struct (implements the traits)\n- `impl TriggerJobArgs for {Kind}Trigger` — sets `TRIGGER_KIND`, `Payload`, `v1_payload_fn`\n\nRequired in `handler_ee.rs`:\n- `#[async_trait] impl TriggerCrud for {Kind}Trigger` with:\n  - `type Trigger = Trigger<{Kind}Config>`\n  - `type TriggerConfigRequest = {Kind}ConfigRequest`\n  - `const ROUTE_PREFIX: &'static str = \"/{kind}_triggers\";`\n  - `const TABLE_NAME`, `ADDITIONAL_SELECT_FIELDS`\n  - `get_deployed_object`, `validate_config`, `create_trigger`, `update_trigger`, `delete_trigger`, `test_connection`\n  - `additional_routes` (optional — mount extra endpoints for things like ARM resource listing, topic discovery)\n\nRegister the crate in `backend/Cargo.toml` as a workspace member and as a dep of `windmill-api` behind the feature flag.\n\n## 3. Wire into `windmill-api` (feature-gated everywhere)\n\n**`backend/windmill-api/src/triggers/handler.rs`** — mount the trigger crate:\n```rust\n#[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n{\n    use crate::triggers::{kind}::{Kind}Trigger;\n    router = router.nest({Kind}Trigger::ROUTE_PREFIX, complete_trigger_routes({Kind}Trigger));\n}\n```\n\n**`backend/windmill-api/src/triggers/{kind}/mod.rs`** — re-export the crate:\n```rust\npub use windmill_trigger_{kind}::*;\n```\n\n**`backend/windmill-api/src/lib.rs`** — if the trigger receives inbound pushes, add a webhook route:\n```rust\n.nest(\"/{kind}/w/{workspace_id}\", {\n    #[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n    { triggers::{kind}::handler_oss::{kind}_push_route_handler() }\n    #[cfg(not(...))]\n    { Router::new() }\n})\n```\n\n## 4. `TriggerKind` enum (`backend/windmill-types/src/triggers.rs`)\n\nAlready has slots for most triggers but verify your variant exists:\n- Add `{Kind}` to the `TriggerKind` enum\n- Add match arm in `to_key()`\n- Add match arm in `from_str`\n- Add match arm in `JobTriggerKind` (if jobs need kind tagging)\n\n## 5. OpenAPI (`backend/windmill-api/openapi.yaml`)\n\nThis file is huge and the single most-forgotten place. Add:\n\n- `/w/{workspace}/{kind}_triggers/create` + `/update/{path}` + `/delete/{path}` + `/get/{path}` + `/list` + `/exists/{path}` + `/setmode/{path}` + `/test` paths (mirror gcp section)\n- Any `additional_routes` your handler exposes (resource discovery, etc.)\n- Schemas: `{Kind}Trigger`, `{Kind}TriggerData`, `{Kind}Mode` (if enum), `{Kind}DeliveryConfig`, helper request/response types\n- Add `{kind}` to `CaptureTriggerKind` enum\n- Add `{kind}_used: boolean` to the `UsedTriggers` response schema\n\nRegenerate frontend client: `npm run generate-backend-client` from `frontend/`.\n\n## 6. `UsedTriggers` + workspace export\n\n**`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.\n\n**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON). The block re-uses the `trigger_ignore_keys` variable so the new kind automatically participates in fork-export stripping (`mode` field is omitted when the source workspace is a fork — keeps fork→parent merges from flipping the parent's enabled state).\n\n**Fork cloning (`clone_triggers_and_schedules` in workspaces.rs)** — add an `INSERT INTO {kind}_trigger ... SELECT ...` block that copies all rows from the parent workspace, forcing `mode = 'disabled'::TRIGGER_MODE`. Always runs at fork creation; forgetting this means users can't carry `{kind}` triggers into their forks.\n\n## 6.5 Hardcoded trigger-kind arrays (silent-failure hotspots)\n\nSeveral files keep **hardcoded arrays** of trigger kind strings. Miss one and ACL checks / user offboarding / trash drop your kind:\n\n- **`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.\n- **`backend/windmill-api-users/src/users.rs`** (`extra_perms_tables`) — which tables get `extra_perms` entries cleaned when a user is deleted.\n- **`backend/windmill-api/src/offboarding.rs`** — three separate arrays (enumeration, fork-copy, and delete paths). **All three** need the new kind.\n- **`backend/windmill-api/src/trash.rs`** — `valid_tables` for the trash / restore API.\n- **`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).\n- **`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.\n- **`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.\n\n**OpenAPI enums** to extend (do NOT forget — generated client will allow it but server rejects as 400):\n- `CaptureTriggerKind` enum\n- Three `kind` enums under `/w/{workspace}/acls/{get,add,remove}/{kind}/{path}` (yes, same list repeated three times)\n\nAfter 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.\n\n## 7. Capture infrastructure (`backend/windmill-api/src/capture.rs`)\n\nIf the trigger supports push delivery, it also needs a capture endpoint so users can test it:\n\n- `{Kind}TriggerConfig` struct (gated by feature flags)\n- `TriggerConfig::{Kind}` variant\n- `set_{kind}_trigger_config` function (creates the subscription/equivalent pointing at the capture URL — use your `manage_{kind}_subscription` helper with `trigger_mode=false`)\n- Both real + no-op versions behind feature gates\n- `TriggerKind::{Kind} => set_{kind}_trigger_config(...)` arm in `set_config`\n- `{kind}_payload` async handler — validates auth (if any), processes payload, calls `insert_capture_payload`\n- Route: `.route(\"/{kind}/{runnable_kind}/{*path}\", post({kind}_payload))` inside `workspaced_unauthed_service` — and expand the surrounding `#[cfg(any(...))]` to include your feature flag\n\n## 8. CLI (`cli/`) — easy to miss, breaks sync silently\n\nCheck all of these:\n\n**`cli/src/types.ts`:**\n- Add `\"{kind}\"` to `TRIGGER_TYPES` array\n- Add `\"{kind}_trigger\"` to `getTypeStrFromPath` return union\n- Add match case in `getTypeStrFromPath`'s `typeEnding ===` chain\n- Add `pushTrigger(\"{kind}\", ...)` branch in `pushObj`\n\n**`cli/src/commands/trigger/trigger.ts`:**\n- Import `{Kind}Trigger` type\n- Add `{kind}: {Kind}Trigger` to the `Trigger` type map\n- Add `{kind}: wmill.get{Kind}Trigger`, `update{Kind}Trigger`, `create{Kind}Trigger` to each function map\n- Add `{kind}: { ... }` template to `triggerTemplates`\n- Add `list{Kind}Triggers` call + spread in the `list` aggregation\n- Update `--kind` option descriptions to mention the new kind\n\n**`cli/src/commands/sync/sync.ts`:**\n- Add `path.endsWith(\".{kind}_trigger\" + ext)` in the file-type filter\n- Add `typ == \"{kind}_trigger\"` in `getTypeOrder`\n- Add `\"{kind}_trigger\"` to the delete-suffix regex (~line 3092)\n- Add a `case \"{kind}_trigger\"` in the delete switch\n\n**`cli/src/guidance/skills.ts`** — **DO NOT EDIT DIRECTLY**. It's auto-generated by `system_prompts/generate.py`. Instead:\n- 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)\n- Then run `python3 system_prompts/generate.py` — it regenerates `cli/src/guidance/skills.ts` with the schema extracted from `backend/windmill-api/openapi.yaml`\n- Commit the regenerated file\n\n## 9. Frontend — editor + drawer\n\nUnder `frontend/src/lib/components/triggers/{kind}/`:\n\n- `{Kind}TriggerPanel.svelte` — the tile shown in the triggers listing\n- `{Kind}TriggerEditor.svelte` — outer drawer wrapper\n- `{Kind}TriggerEditorInner.svelte` — state + business logic; must expose:\n  - `openEdit(path, isFlow, defaultValues?)` method\n  - `isEditor` prop, `onConfigChange` + `onCaptureConfigChange` callbacks\n  - `get{Kind}Config()` + `get{Kind}CaptureConfig()` helpers\n  - `captureConfig = $derived.by(untrack(() => isEditor) ? get{Kind}CaptureConfig : () => ({}))`\n  - `$effect(() => { const args = [captureConfig, isValid] as const; untrack(() => onCaptureConfigChange?.(...args)) })`\n- `{Kind}TriggerEditorConfigSection.svelte` — form fields; use design-system components (`TextInput`, `Select`, `Toggle`, `ToggleButtonGroup`), never raw `<input>`\n- `{Kind}Capture.svelte` — capture panel; wraps `CaptureSection` with `captureType=\"{kind}\"`\n- `utils.ts` — `requestBody` builders and any trigger-type-specific helpers\n\n## 10. Frontend — global integration\n\nEasy to miss:\n\n- **`frontend/src/lib/components/triggers.ts`** — add `'{kind}'` to the `TriggerKind` union\n- **`frontend/src/lib/components/triggers/CaptureWrapper.svelte`**:\n  - Import `{Kind}Capture`\n  - Add to `isStreamingCapture()` array (streaming = pull-style; push-style is typically `false`)\n  - Add `{:else if captureType === '{kind}'}` branch with the `<{Kind}Capture>` render\n- **`frontend/src/lib/components/sidebar/SidebarContent.svelte`** — import the icon, add the nav entry\n- **`frontend/src/lib/components/sidebar/OperatorMenu.svelte`** — add the operator-mode entry\n- **`frontend/src/routes/(root)/(logged)/+layout.svelte`** — destructure `{kind}_used` from `/get_used_triggers` response, push `'{kind}'` into `usedKinds`\n- **`frontend/src/lib/components/search/GlobalSearchModal.svelte`** — import icon, add \"Go to {Kind} ...\" entry\n- **`frontend/src/lib/components/offboarding-utils.ts`** — add mappings `{kind}_trigger: '{kind}_triggers'` and `{kind}_trigger: '{kind} trigger'`\n- **`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)\n- **`frontend/src/routes/(root)/(logged)/{kind}_triggers/+page.svelte`** — listing page (mirror `gcp_triggers/+page.svelte` for push+pull, `kafka_triggers` for pure streaming)\n- **`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\n\n## 10.5 AI system prompts (`system_prompts/`)\n\n- **`system_prompts/utils.py`** — append `('{Kind}Trigger', '{kind}_trigger')` to `SCHEMA_MAPPINGS['triggers']` (master list used by code generation + CLI skills)\n- **`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\n- **`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)\n- Run `python3 system_prompts/generate.py` — this rewrites `cli/src/guidance/skills.ts` and all `auto-generated/` docs. Commit the regenerated files\n\n## 11. Validation\n\nRun all of these before declaring done:\n\n```bash\n# Backend\ncd backend\ncargo check --features enterprise,{kind}_trigger,private   # minimal\ncargo check --features enterprise,azure_trigger,private,gcp_trigger,http_trigger,mqtt_trigger,postgres_trigger,sqs_trigger,kafka,nats,smtp,websocket  # full\n\n# SQLx offline data (never run `cargo sqlx prepare` directly — use the wrapper)\n./update_sqlx.sh\n\n# Frontend\ncd frontend\nnpm run generate-backend-client\nnpm run check:fast\n```\n\nSmoke test in the UI: create a trigger, save, check it appears in sidebar + search, delete, re-create via CLI `wmill sync`.\n\n## 12. Common pitfalls\n\n- **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\n- **`.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`\n- **`on:event` directives** — legacy Svelte 4, no-op in runes mode. Use callback props (`onSelected`, `onConfigChange`)\n- **`$bindable(default_value)` on optional props** — banned by project CLAUDE.md. Use `$bindable()` + `$derived(prop ?? default)` instead\n- **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)\n- **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\n- **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\n- **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\n- **Workspace-scoped tag compatibility** — if the trigger has tags, verify forked workspaces handle them (see commit `0773b5bc85` for a historical fix)\n\n## 13. EE file split\n\nIf 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.\n\n## 14. Final checklist before PR\n\n- [ ] Migration up/down tested (revert + re-apply)\n- [ ] `./update_sqlx.sh` committed the updated `.sqlx/` offline data\n- [ ] `cargo check` passes with your feature flag + with all trigger features\n- [ ] `npm run check:fast` passes\n- [ ] Trigger visible in sidebar with correct icon weight (not oversized/colored — use `currentColor`)\n- [ ] Create, edit, delete flow all work in the UI\n- [ ] Capture button works (if push-capable)\n- [ ] Trigger appears in `/get_used_triggers` → sidebar pulse\n- [ ] `wmill sync pull` + `wmill sync push` both round-trip the trigger\n- [ ] `wmill trigger list` includes it\n- [ ] OpenAPI schemas are complete (no `null` in generated types)\n",".agents/skills/local-review/SKILL.md":"---\nname: local-review\ndescription: Code review the current PR (or branch diff against main) for bugs, security, and AGENTS.md compliance. MUST use when asked to review code.\n---\n\n# Local Code Review\n\nRun the same review locally that the GitHub auto-review actions run on PRs (Claude / Codex / Pi). The review policy lives in `REVIEW.md`.\n\n**Why a subagent**: the review MUST run in a fresh context — not inline in the current session. If the user has been iterating on the diff, the main session has absorbed their reasoning and rationalizations, so it anchors and misses things CI catches. A subagent starts cold, like CI does.\n\n## Steps\n\n1. **Determine the PR scope** (cheap, do this in the main session):\n   - If an argument is provided, treat it as a PR number or branch.\n   - Otherwise, detect from the current branch vs `main`.\n   - Confirm the PR/branch exists (`gh pr view <n>` or `git rev-parse <branch>`).\n\n2. **Delegate the review to a fresh-context subagent** with a self-contained prompt. The prompt MUST include:\n   - The PR number or branch name to review.\n   - The instruction to read `REVIEW.md` first for the policy, then `AGENTS.md` files in directories touched by the diff.\n   - The exact output format (see below).\n   - Whether `--comment` was requested (so the subagent emits inline-comment payloads if needed).\n   - Any \"Additional reviewer instructions\" the user provided.\n\n   - **Claude Code**: use the `Agent` tool with `subagent_type: branch-diff-reviewer` (read-only tools, purpose-built for this). If unavailable, fall back to `general-purpose`.\n   - **Codex / Pi**: if the CLI exposes a fresh-session subagent mechanism, use it. Otherwise tell the user to run the skill in a fresh CLI session and stop — running inline in the current session defeats the purpose.\n\n3. **Receive the findings** from the subagent and relay them to the user verbatim. Do not re-summarize, re-judge, or filter — the whole point of fresh context is to surface what the main session would dismiss.\n\n4. **Post comments if `--comment` was requested**: use the `gh` commands below with the subagent's output as the body. The main session does the posting because the subagent is read-only.\n\n## Subagent prompt template\n\n```\nReview <PR #N | branch X> against main per the policy in REVIEW.md.\n\nSteps:\n1. Read REVIEW.md (repo root) for the full policy: severity triage, public-surface\n   checklist, AGENTS.md compliance, test coverage assessment.\n2. Read AGENTS.md (repo root) and any AGENTS.md in directories touched by the diff.\n3. Get the diff: `gh pr diff <N>` (if PR) or `git diff main...<branch>`.\n4. Get context: `gh pr view <N>` (if PR) or `git log main..<branch> --oneline`.\n5. Read changed files only when the diff alone is insufficient to validate a finding.\n6. Self-validate each finding: \"is this definitely a real issue a senior engineer\n   would flag?\" Discard if uncertain.\n7. Output findings in the exact format below. Do not modify any files.\n\n<paste output format from below>\n\n<if --comment requested:>\nAdditionally emit a JSON array of inline comments suitable for the GitHub reviews\nAPI, one per finding that maps to a specific line:\n[{\"path\": \"...\", \"line\": N, \"side\": \"RIGHT\", \"body\": \"[P1] ...\"}, ...]\n```\n\n## Output format\n\n```\n## Code review\n\n<verdict line per REVIEW.md>\n\nFound N issues:\n\n1. [P0|P1|P2] <description>\n   <file_path:line_number>\n\n2. [P0|P1|P2] <description>\n   <file_path:line_number>\n```\n\nEnd with a `Test coverage` section per the shared policy.\n\nIf no issues are found:\n\n```\n## Code review\n\nGood to merge.\n\nNo issues found. Checked for bugs, security, and AGENTS.md compliance.\n```\n\n## Posting comments (`--comment`)\n\nFor a top-level PR comment:\n\n```bash\ngh pr review --comment --body \"<summary from subagent>\"\n```\n\nFor inline comments on specific lines (using the JSON the subagent emitted):\n\n```bash\ngh api repos/{owner}/{repo}/pulls/{pr}/reviews \\\n  -f body=\"<summary>\" -f event=\"COMMENT\" -f comments=\"<json from subagent>\"\n```\n",".agents/skills/improve-codebase-architecture/SKILL.md":"---\nname: improve-codebase-architecture\ndescription: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.\ndisable-model-invocation: true\n---\n\n# Improve Codebase Architecture\n\nSurface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.\n\nThis command is _informed_ by the project's domain model and built on a shared design vocabulary:\n\n- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, \"the interface is the test surface\", \"one adapter = hypothetical seam, two = real\"). Use these terms exactly in every suggestion — don't drift into \"component,\" \"service,\" \"API,\" or \"boundary.\"\n- The domain language in `CONTEXT.md` gives names to good seams.\n\n## Process\n\n### 1. Explore\n\n**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:\n\n- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.\n- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.\n\nRead the project's domain glossary (`CONTEXT.md`) first.\n\nThen spawn a sub-agent to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:\n\n- Where does understanding one concept require bouncing between many small modules?\n- Where are modules **shallow** — interface nearly as complex as the implementation?\n- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?\n- Where do tightly-coupled modules leak across their seams?\n- Which parts of the codebase are untested, or hard to test through their current interface?\n\nApply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A \"yes, concentrates\" is the signal you want.\n\n### 2. Present candidates as an HTML report\n\nWrite a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `<tmpdir>/architecture-review-<timestamp>.html` so each run gets a fresh file. Open it for the user — `xdg-open <path>` on Linux, `open <path>` on macOS, `start <path>` on Windows — and tell them the absolute path.\n\nThe report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.\n\nFor each candidate, render a card with:\n\n- **Files** — which files/modules are involved\n- **Problem** — why the current architecture is causing friction\n- **Solution** — plain English description of what would change\n- **Benefits** — explained in terms of locality and leverage, and how tests would improve\n- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening\n- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge\n\nEnd the report with a **Top recommendation** section: which candidate you'd tackle first and why.\n\n**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines \"Order,\" talk about \"the Order intake module\" — not \"the FooBarHandler,\" and not \"the Order service.\"\n\nSee `.agents/skills/improve-codebase-architecture/HTML-REPORT.md` (path from the repo root) for the full HTML scaffold, diagram patterns, and styling guidance.\n\nDo NOT propose interfaces yet. After the file is written, ask the user: \"Which of these would you like to explore?\"\n\n### 3. Grilling loop\n\nOnce the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.\n\nSide effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:\n\n- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist.\n- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.\n- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.\n",".agents/skills/local-review-codex/SKILL.md":"---\nname: local-review-codex\ndescription: Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy, model and reasoning effort as the codex-pr-review GitHub action.\n---\n\n# Local Codex Review (pre-push)\n\nRuns the exact same review Codex performs in CI (`.github/workflows/codex-pr-review.yml`),\nbut locally and scoped to work you have not pushed yet — so you catch what CI would flag\nbefore the PR exists. Use this before `git push` on a non-trivial change.\n\n**Correspondence with CI** — identical:\n- Policy: `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test coverage).\n- Model: `gpt-6.1-sol`.\n- Reasoning effort: `model_reasoning_effort=\"xhigh\"`.\n- Output: markdown starting with `## Codex Review`, findings tagged P0 / P1 / P2 with file:line.\n\n**Differences from CI** — local-only:\n- Scope is the current branch vs `main` at the merge-base, **including uncommitted changes** (CI reviews a pushed PR diff).\n- Sandbox is `read-only` (CI uses `danger-full-access` on an ephemeral runner). Codex reads the diff and files but cannot modify your working tree.\n- Fresh context is inherent: `codex exec` is a separate cold process, so it does not anchor on the current chat session — the same reason `local-review` insists on a subagent.\n\n## Prerequisites\n\n- `codex` CLI **>= 0.159.3** installed and authed via `codex login` or an `OPENAI_API_KEY` in the environment (which takes priority). Older CLIs reject the model with \"requires a newer version of Codex\"; `run.sh` checks the version up front. Upgrade with `npm install --global @openai/codex@0.159.3` (may need `sudo` for a global install). This matches the pin in `.github/workflows/codex-pr-review.yml` — the CLI version is the same on both sides.\n- `git fetch` the base ref if it's stale, so the merge-base is accurate.\n\n## Run\n\n```bash\nbash .agents/skills/local-review-codex/run.sh          # review vs main (default)\nbash .agents/skills/local-review-codex/run.sh <base>   # review vs a different base ref\n```\n\nInvoke with `bash` (or run the executable directly) — the script needs Bash for\n`set -o pipefail`; `sh` is Dash on Debian/Ubuntu and would fail. If `main` isn't a\nlocal branch (e.g. a fresh single-branch checkout), the runner falls back to\n`origin/main` automatically.\n\nThe script computes `BASE_SHA = git merge-base HEAD <base>`, feeds Codex `REVIEW.md` plus a\ndiff context pointing at `git diff <BASE_SHA>` (which folds in uncommitted edits), and prints\nthe review. It writes only temp files — nothing lands in the working tree.\n\n## Relaying the result\n\nPrint the Codex output verbatim. Do not re-summarize or filter it — the value of a cold Codex\npass is surfacing what the current session would rationalize away. Then decide with the user\nwhether to address findings before pushing.\n\nFor a Claude-native review instead, use `local-review` (branch-diff-reviewer subagent). This\nskill is the Codex counterpart; run both for independent perspectives.\n",".agents/skills/native-trigger/SKILL.md":"---\nname: native-trigger\ndescription: Guidance for adding native trigger services to Windmill. Use when implementing or modifying native trigger integrations across the backend and frontend.\n---\n\n# Skill: Adding Native Trigger Services\n\nThis skill provides comprehensive guidance for adding new native trigger services to Windmill. Native triggers allow external services (like Nextcloud, Google Drive, etc.) to trigger Windmill scripts/flows via webhooks or push notifications.\n\n## Architecture Overview\n\nThe native trigger system consists of:\n\n1. **Database Layer** - PostgreSQL tables and enum types\n2. **Backend Rust Implementation** - Core trait, handlers, and service modules in the `windmill-native-triggers` crate\n3. **Frontend Svelte Components** - Configuration forms and UI components\n\n### Key Files\n\n| Component | Path |\n|-----------|------|\n| Core module with `External` trait | `backend/windmill-native-triggers/src/lib.rs` |\n| Generic CRUD handlers | `backend/windmill-native-triggers/src/handler.rs` |\n| Background sync logic | `backend/windmill-native-triggers/src/sync.rs` |\n| OAuth/workspace integration | `backend/windmill-native-triggers/src/workspace_integrations.rs` |\n| Re-export shim (windmill-api) | `backend/windmill-api/src/native_triggers/mod.rs` |\n| TriggerKind enum | `backend/windmill-common/src/triggers.rs` |\n| JobTriggerKind enum | `backend/windmill-common/src/jobs.rs` |\n| Frontend service registry | `frontend/src/lib/components/triggers/native/utils.ts` |\n| Frontend trigger utilities | `frontend/src/lib/components/triggers/utils.ts` |\n| Trigger badges (icons + counts) | `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte` |\n| Workspace integrations UI | `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte` |\n| OAuth config form component | `frontend/src/lib/components/workspaceSettings/OAuthClientConfig.svelte` |\n| OpenAPI spec | `backend/windmill-api/openapi.yaml` |\n| Reference: Nextcloud module | `backend/windmill-native-triggers/src/nextcloud/` |\n| Reference: Google module | `backend/windmill-native-triggers/src/google/` |\n\n### Crate Structure\n\nThe native trigger code lives in the `windmill-native-triggers` crate (`backend/windmill-native-triggers/`). The `windmill-api` crate re-exports everything via a shim:\n\n```rust\n// backend/windmill-api/src/native_triggers/mod.rs\npub use windmill_native_triggers::*;\n```\n\nAll new service modules go in `backend/windmill-native-triggers/src/`.\n\n---\n\n## Core Concepts\n\n### The `External` Trait\n\nEvery native trigger service implements the `External` trait defined in `lib.rs`:\n\n```rust\n#[async_trait]\npub trait External: Send + Sync + 'static {\n    // Associated types:\n    type ServiceConfig: Debug + DeserializeOwned + Serialize + Send + Sync;\n    type TriggerData: Debug + Serialize + Send + Sync;\n    type OAuthData: DeserializeOwned + Serialize + Clone + Send + Sync;\n    type CreateResponse: DeserializeOwned + Send + Sync;\n\n    // Constants:\n    const SUPPORT_WEBHOOK: bool;\n    const SERVICE_NAME: ServiceName;\n    const DISPLAY_NAME: &'static str;\n    const TOKEN_ENDPOINT: &'static str;\n    const REFRESH_ENDPOINT: &'static str;\n    const AUTH_ENDPOINT: &'static str;\n\n    // Required methods:\n    async fn create(&self, w_id, oauth_data, webhook_token, data, db, tx) -> Result<Self::CreateResponse>;\n    async fn update(&self, w_id, oauth_data, external_id, webhook_token, data, db, tx) -> Result<serde_json::Value>;\n    async fn get(&self, w_id, oauth_data, external_id, db, tx) -> Result<Self::TriggerData>;\n    async fn delete(&self, w_id, oauth_data, external_id, db, tx) -> Result<()>;\n    async fn exists(&self, w_id, oauth_data, external_id, db, tx) -> Result<bool>;\n    async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors);\n    fn external_id_and_metadata_from_response(&self, resp) -> (String, Option<serde_json::Value>);\n\n    // Methods with defaults:\n    async fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned>;\n    fn service_config_from_create_response(&self, data, resp) -> Option<serde_json::Value>;\n    fn additional_routes(&self) -> axum::Router;\n    async fn http_client_request<T, B>(&self, url, method, workspace_id, connection_path, db, headers, body) -> Result<T>;\n}\n```\n\nKey design points:\n- **`update()` returns `serde_json::Value`** - the resolved service_config to store. Each service is responsible for building the final config.\n- **`maintain_triggers()`** - periodic background maintenance. Each service implements its own strategy (Nextcloud: reconcile with external state; Google: renew expiring channels).\n- **No `list_all()` in the trait** - services that need it (Nextcloud) implement it privately; services that don't (Google) use different maintenance strategies.\n- **No `get_external_id_from_trigger_data()` or `extract_service_config_from_trigger_data()`** - removed in favor of the `maintain_triggers` pattern.\n\n### Create Lifecycle: Two Paths\n\nThe `create_native_trigger` handler in `handler.rs` supports two creation flows, controlled by `service_config_from_create_response()`:\n\n**Path A: Short (Google pattern)** - `service_config_from_create_response()` returns `Some(config)`:\n1. `create()` registers on external service\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `service_config_from_create_response()` builds the config directly from input data + response metadata\n4. Stores trigger in DB -- done, no extra round-trip\n\nUse this when the external_id is known before the create call (e.g., Google generates the channel_id as a UUID upfront and includes it in the webhook URL).\n\n**Path B: Long (Nextcloud pattern)** - `service_config_from_create_response()` returns `None` (default):\n1. `create()` registers on external service (webhook URL has no external_id yet)\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `update()` is called to fix the webhook URL with the now-known external_id\n4. `update()` returns the resolved service_config\n5. Stores trigger in DB\n\nUse this when the external_id is assigned by the remote service and the webhook URL needs to be corrected after creation.\n\n### OAuth App and Connections\n\nThe OAuth **app** (client) is configured per workspace and service; **connections** (connected\naccounts) are many per service. Each trigger acts through one connection, named by\n`native_trigger.connection_path`:\n\n| Table | What's Stored |\n|-------|---------------|\n| `workspace_integrations` | `oauth_data` JSON with `base_url`, `client_id`, `client_secret`, `instance_shared` flag. One row per (workspace, service); it must exist for every connection, since token refresh reads the client from it |\n| `variable` | Encrypted `access_token` at the connection path, linked to `account` via its `account` column |\n| `account` | `refresh_token`, with `client` = service name and `is_workspace_integration = true` |\n| `resource` | The connection's resource at the same path, `{ \"token\": \"$var:<path>\" }` |\n\nA user may use a connection when they can read its variable (checked with a user transaction,\nso the variable's row-level security decides): `resolve_usable_connection` /\n`list_usable_connections` in `lib.rs`. The exception is the **workspace connection**: an admin\nmay share one connection per service (`workspace_integrations.resource_path`, set by\n`POST .../workspace_connection`), and every member may then use it without reading its token.\nRenames carry the marker along; disconnecting clears it. Background work (sync, renewal, rename re-registration,\nwebhook handling) uses the stored `connection_path` directly.\n\n`decrypt_oauth_data(db, w_id, service, connection_path)` assembles these into one struct, and\nevery service's `OAuthData` carries `connection_path` so `http_client_request` can refresh and\nwrite back the right connection:\n```rust\npub struct OAuthConfig {\n    pub base_url: String,\n    pub access_token: String,      // decrypted from the connection's variable\n    pub refresh_token: Option<String>, // from its account\n    pub client_id: String,         // from oauth_data or instance settings\n    pub client_secret: String,     // from oauth_data or instance settings\n    pub connection_path: String,\n}\n```\n\nA refreshed token is written back by connection path and account id, never by service: providers\nlike Nextcloud rotate refresh tokens, so writing one onto another account breaks its next refresh.\n\nInstance-level sharing: when the workspace has no OAuth app of its own and the instance admin\nshares one (`share_with_workspaces` in the `oauths` setting; not available for Nextcloud), the\nconnect flow uses it and records `{\"instance_shared\": true}` in `workspace_integrations`.\n\n### URL Resolution\n\nThe `resolve_endpoint()` helper handles both absolute and relative OAuth URLs:\n\n```rust\npub fn resolve_endpoint(base_url: &str, endpoint: &str) -> String {\n    if endpoint.starts_with(\"http://\") || endpoint.starts_with(\"https://\") {\n        endpoint.to_string()  // Google: absolute URLs\n    } else {\n        format!(\"{}{}\", base_url, endpoint)  // Nextcloud: relative paths\n    }\n}\n```\n\n### ServiceName Methods\n\n`ServiceName` is the central registry enum. Each variant must implement these match arms:\n\n| Method | Purpose |\n|--------|---------|\n| `as_str()` | Lowercase identifier (e.g., `\"google\"`) |\n| `as_trigger_kind()` | Maps to `TriggerKind` enum |\n| `as_job_trigger_kind()` | Maps to `JobTriggerKind` enum |\n| `token_endpoint()` | OAuth token endpoint (relative or absolute) |\n| `auth_endpoint()` | OAuth authorization endpoint |\n| `oauth_scopes()` | Space-separated OAuth scopes |\n| `resource_type()` | Resource type for token storage (e.g., `\"gworkspace\"`) |\n| `extra_auth_params()` | Extra OAuth params (e.g., Google needs `access_type=offline`, `prompt=consent`) |\n| `integration_service()` | Maps to the workspace integration service (usually `*self`) |\n| `TryFrom<String>` | Parse from string |\n| `Display` | Delegates to `as_str()` |\n\n---\n\n## Step-by-Step Implementation Guide\n\n### Step 1: Database Migration\n\nCreate a new migration file: `backend/migrations/YYYYMMDDHHMMSS_newservice_trigger.up.sql`\n\n```sql\n-- Add the service to the native_trigger_service enum\nALTER TYPE native_trigger_service ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to TRIGGER_KIND enum (used for trigger tracking)\nALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to job_trigger_kind enum (used for job tracking)\nALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'newservice';\n```\n\nAlso create the corresponding down migration.\n\n### Step 2: Update windmill-common Enums\n\n#### `backend/windmill-common/src/triggers.rs`\n\nAdd variant to `TriggerKind` enum, and update `to_key()` and `fmt()` implementations.\n\n#### `backend/windmill-common/src/jobs.rs`\n\nAdd variant to `JobTriggerKind` enum and update the `Display` implementation.\n\n### Step 3: Backend Service Module\n\nCreate a new directory: `backend/windmill-native-triggers/src/newservice/`\n\n#### `mod.rs` - Type Definitions\n\n```rust\nuse serde::{Deserialize, Serialize};\n\npub mod external;\n// pub mod routes; // Only if you need additional service-specific routes\n\n/// OAuth data deserialized from the three-table pattern.\n/// The actual structure is built by decrypt_oauth_data() from variable + account + workspace_integrations.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct NewServiceOAuthData {\n    pub base_url: String,              // from workspace_integrations.oauth_data\n    pub access_token: String,          // decrypted from variable table\n    pub refresh_token: Option<String>, // from account table\n    // Note: client_id and client_secret are in OAuthConfig, not here\n    // unless the service needs them at runtime for API calls\n}\n\n/// Configuration provided by user when creating/updating a trigger.\n/// Stored as JSON in native_trigger.service_config.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceConfig {\n    // Service-specific configuration fields\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n}\n\n/// Data retrieved from the external service about a trigger.\n/// Returned by the get() method and shown in the UI.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceTriggerData {\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n    // Fields that shouldn't affect service_config comparison should use #[serde(skip_serializing)]\n}\n\n/// Response from external service when creating a trigger/webhook.\n#[derive(Debug, Deserialize)]\npub struct CreateTriggerResponse {\n    pub id: String,\n}\n\n/// Handler struct (stateless, used for routing)\n#[derive(Copy, Clone)]\npub struct NewService;\n```\n\n#### `external.rs` - External Trait Implementation\n\n```rust\nuse async_trait::async_trait;\nuse reqwest::Method;\nuse sqlx::PgConnection;\nuse std::collections::HashMap;\nuse windmill_common::{\n    error::{Error, Result},\n    BASE_URL, DB,\n};\n\nuse crate::{\n    generate_webhook_service_url, External, NativeTrigger, NativeTriggerData, ServiceName,\n    sync::{SyncError, TriggerSyncInfo},\n};\nuse super::{NewService, NewServiceConfig, NewServiceOAuthData, NewServiceTriggerData, CreateTriggerResponse};\n\n#[async_trait]\nimpl External for NewService {\n    type ServiceConfig = NewServiceConfig;\n    type TriggerData = NewServiceTriggerData;\n    type OAuthData = NewServiceOAuthData;\n    type CreateResponse = CreateTriggerResponse;\n\n    const SERVICE_NAME: ServiceName = ServiceName::NewService;\n    const DISPLAY_NAME: &'static str = \"New Service\";\n    const SUPPORT_WEBHOOK: bool = true;\n    const TOKEN_ENDPOINT: &'static str = \"/oauth/token\";\n    const REFRESH_ENDPOINT: &'static str = \"/oauth/token\";\n    const AUTH_ENDPOINT: &'static str = \"/oauth/authorize\";\n\n    async fn create(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::CreateResponse> {\n        let base_url = &*BASE_URL.read().await;\n\n        // external_id is None during create (we get it from the response)\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            None, Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/create\", oauth_data.base_url);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let response: CreateTriggerResponse = self\n            .http_client_request(&url, Method::POST, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        Ok(response)\n    }\n\n    /// Update returns the resolved service_config as JSON.\n    /// For services using the update+get pattern, call self.get() and serialize.\n    async fn update(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<serde_json::Value> {\n        let base_url = &*BASE_URL.read().await;\n\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            Some(external_id), Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let _: serde_json::Value = self\n            .http_client_request(&url, Method::PUT, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        // Fetch back the updated state to get the resolved config\n        let trigger_data = self.get(w_id, oauth_data, external_id, db, tx).await?;\n        serde_json::to_value(&trigger_data)\n            .map_err(|e| Error::InternalErr(format!(\"Failed to serialize trigger data: {}\", e)))\n    }\n\n    async fn get(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::TriggerData> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        self.http_client_request::<_, ()>(&url, Method::GET, w_id, &oauth_data.connection_path, db, None, None).await\n    }\n\n    async fn delete(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<()> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let _: serde_json::Value = self\n            .http_client_request::<_, ()>(&url, Method::DELETE, w_id, &oauth_data.connection_path, db, None, None)\n            .await\n            .or_else(|e| match &e {\n                Error::InternalErr(msg) if msg.contains(\"404\") => Ok(serde_json::Value::Null),\n                _ => Err(e),\n            })?;\n        Ok(())\n    }\n\n    async fn exists(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<bool> {\n        match self.get(w_id, oauth_data, external_id, db, tx).await {\n            Ok(_) => Ok(true),\n            Err(Error::NotFound(_)) => Ok(false),\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Background maintenance. Choose the right pattern for your service:\n    /// - For services with queryable external state: use reconcile_with_external_state()\n    /// - For channel-based services with expiration: implement renewal logic\n    async fn maintain_triggers(\n        &self,\n        db: &DB,\n        workspace_id: &str,\n        triggers: &[NativeTrigger],\n        oauth_data: &Self::OAuthData,\n        synced: &mut Vec<TriggerSyncInfo>,\n        errors: &mut Vec<SyncError>,\n    ) {\n        // Option A: Reconcile with external state (Nextcloud pattern)\n        // Fetch all triggers from external service and compare with DB\n        let external_triggers = match self.list_all(workspace_id, oauth_data, db).await {\n            Ok(triggers) => triggers,\n            Err(e) => {\n                errors.push(SyncError {\n                    resource_path: format!(\"workspace:{}\", workspace_id),\n                    error_message: format!(\"Failed to list triggers: {}\", e),\n                    error_type: \"api_error\".to_string(),\n                });\n                return;\n            }\n        };\n\n        // Convert to (external_id, config_json) pairs\n        let external_pairs: Vec<(String, serde_json::Value)> = external_triggers\n            .into_iter()\n            .map(|t| (t.id.clone(), serde_json::to_value(&t).unwrap_or_default()))\n            .collect();\n\n        crate::sync::reconcile_with_external_state(\n            db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n        ).await;\n    }\n\n    fn external_id_and_metadata_from_response(\n        &self,\n        resp: &Self::CreateResponse,\n    ) -> (String, Option<serde_json::Value>) {\n        (resp.id.clone(), None)\n    }\n\n    // service_config_from_create_response: NOT overridden (returns None).\n    // This means the handler uses the update+get pattern after create.\n    // Override and return Some(...) to skip the update+get cycle (Google pattern).\n}\n\nimpl NewService {\n    /// Private helper to list all triggers from the external service.\n    async fn list_all(\n        &self,\n        w_id: &str,\n        oauth_data: &<Self as External>::OAuthData,\n        db: &DB,\n    ) -> Result<Vec<<Self as External>::TriggerData>> {\n        // Implementation depends on the external service's API\n        todo!()\n    }\n}\n```\n\n### Step 4: Update lib.rs Registry\n\nIn `backend/windmill-native-triggers/src/lib.rs`:\n\n```rust\n// Service modules - add new services here:\n#[cfg(feature = \"native_trigger\")]\npub mod newservice;  // <-- Add this\n\n// ServiceName enum - add variant:\npub enum ServiceName {\n    Nextcloud,\n    Google,\n    NewService,  // <-- Add this\n}\n\n// Then add match arms in ALL ServiceName methods:\n// as_str(), as_trigger_kind(), as_job_trigger_kind(), token_endpoint(),\n// auth_endpoint(), oauth_scopes(), resource_type(), extra_auth_params(),\n// integration_service(), TryFrom<String>, Display\n```\n\n### Step 5: Update handler.rs Routes\n\nIn `backend/windmill-native-triggers/src/handler.rs`:\n\n```rust\npub fn generate_native_trigger_routers() -> Router {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n        return router\n            .nest(\"/nextcloud\", service_routes(NextCloud))\n            .nest(\"/google\", service_routes(Google))\n            .nest(\"/newservice\", service_routes(NewService));  // <-- Add this\n    }\n    // ...\n}\n```\n\n### Step 6: Update sync.rs\n\nIn `backend/windmill-native-triggers/src/sync.rs`:\n\n```rust\npub async fn sync_all_triggers(db: &DB) -> Result<BackgroundSyncResult> {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n\n        // ... existing service syncs ...\n\n        // New service sync\n        let (service_name, result) = sync_service_triggers(db, NewService).await;\n        total_synced += result.synced_triggers.len();\n        total_errors += result.errors.len();\n        service_results.insert(service_name, result);\n    }\n    // ...\n}\n```\n\n### Step 7: Frontend Service Registry\n\nIn `frontend/src/lib/components/triggers/native/utils.ts`:\n\nAdd to `NATIVE_TRIGGER_SERVICES`, `getTriggerIconName()`, and `getServiceIcon()`.\n\n### Step 8: Frontend Trigger Form Component\n\nCreate: `frontend/src/lib/components/triggers/native/services/newservice/NewServiceTriggerForm.svelte`\n\n### Step 9: Frontend Icon Component\n\nCreate: `frontend/src/lib/components/icons/NewServiceIcon.svelte`\n\n### Step 10: Update NativeTriggerEditor\n\nCheck `frontend/src/lib/components/triggers/native/NativeTriggerEditor.svelte` to ensure it dynamically loads form components based on service name.\n\n### Step 11: Workspace Integration UI\n\nAdd your service to the `supportedServices` map in `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte`:\n\n```typescript\nconst supportedServices: Record<string, ServiceConfig> = {\n    // ... existing services ...\n    newservice: {\n        name: 'newservice',\n        displayName: 'New Service',\n        description: 'Connect to New Service for triggers',\n        icon: NewServiceIcon,\n        docsUrl: 'https://www.windmill.dev/docs/integrations/newservice',\n        requiresBaseUrl: false,  // false for cloud services, true for self-hosted\n        setupInstructions: [\n            'Step 1: Create an OAuth app on the service',\n            'Step 2: Configure the redirect URI shown below',\n            'Step 3: Enter the client credentials below'\n        ]\n    }\n}\n```\n\n### Step 12: Update `frontend/src/lib/components/triggers/utils.ts`\n\nUpdate ALL of these maps/functions:\n1. `triggerIconMap` - import and add icon\n2. `triggerDisplayNamesMap` - add display name\n3. `triggerTypeOrder` in `sortTriggers()` - add type\n4. `getLightConfig()` - add case for your service\n5. `getTriggerLabel()` - add case for your service\n6. `jobTriggerKinds` - add to array\n7. `countPropertyMap` - add count property\n8. `triggerSaveFunctions` - add save function\n\n### Step 13: Update TriggersBadge Component\n\nIn `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte`:\n\n1. Import the icon\n2. Add to `baseConfig` with `countKey` (the dynamic `availableNativeServices` loop does NOT set `countKey`)\n3. Add to the `allTypes` array\n\n### Step 14: Update TriggersWrapper.svelte\n\nIn `frontend/src/lib/components/triggers/TriggersWrapper.svelte`:\n\nAdd a `{:else if selectedTrigger.type === 'yourservice'}` case that renders `<NativeTriggersPanel service=\"yourservice\" ...>` with the same props pattern as the existing native trigger cases (e.g., `nextcloud`).\n\n### Step 15: Update AddTriggersButton.svelte\n\nIn `frontend/src/lib/components/triggers/AddTriggersButton.svelte`:\n\n1. Add `yourserviceAvailable` state variable\n2. Add `setYourserviceState()` async function using `isServiceAvailable('yourservice', $workspaceStore!)`\n3. Call it at module level\n4. Add a dropdown entry to `addTriggerItems` with `hidden: !yourserviceAvailable`\n\n### Step 16: Update TriggersEditor.svelte Delete Handling\n\nIn `frontend/src/lib/components/triggers/TriggersEditor.svelte`:\n\nAdd your service to the `nativeTriggerServices` map in `deleteDeployedTrigger()`. Native triggers use `NativeTriggerService.deleteNativeTrigger({ workspace, serviceName, externalId })` instead of the standard `path`-based delete.\n\n### Step 17: Update `getUsedTriggers` for Sidebar Visibility\n\nThe sidebar (`frontend/src/lib/components/sidebar/SidebarContent.svelte`) shows native-trigger links only if `$usedTriggerKinds` includes the service — without this, your trigger page will never appear in the nav bar even when triggers exist.\n\n1. **Backend** — add `{service}_used: bool` to the `UsedTriggers` struct and SELECT in `backend/windmill-api-workspaces/src/workspaces.rs::get_used_triggers()`:\n   ```rust\n   EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = '{service}'::native_trigger_service) AS \"{service}_used!\"\n   ```\n2. **OpenAPI** — add `{service}_used: boolean` to the response schema for `GET /w/{workspace}/workspaces/used_triggers` (under both `properties` and `required`).\n3. **Layout** — in `frontend/src/routes/(root)/(logged)/+layout.svelte::loadUsedTriggerKinds()`, destructure `{service}_used` and push `'{service}'` to `usedKinds`.\n\n### Step 18: Update OpenAPI Spec and Regenerate Types\n\nAdd to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:\n\n```bash\ncd frontend && npm run generate-backend-client\n```\n\n---\n\n## Special Patterns\n\n### Unified Service with `trigger_type` (Google Pattern)\n\nWhen a single service handles multiple trigger types (e.g., Google Drive + Calendar share OAuth and API patterns), use a single `ServiceName` variant with a discriminator field:\n\n```rust\npub enum GoogleTriggerType { Drive, Calendar }\n\npub struct GoogleServiceConfig {\n    pub trigger_type: GoogleTriggerType,\n    // Drive-specific fields (only used when trigger_type = Drive)\n    pub resource_id: Option<String>,\n    pub resource_name: Option<String>,\n    // Calendar-specific fields (only used when trigger_type = Calendar)\n    pub calendar_id: Option<String>,\n    pub calendar_name: Option<String>,\n    // Metadata set after creation\n    pub google_resource_id: Option<String>,\n    pub expiration: Option<String>,\n}\n```\n\nBranch in trait methods based on `trigger_type`. Frontend uses a `ToggleButtonGroup` to switch between types. This keeps the codebase simpler (one service, one OAuth flow, one set of routes).\n\nSee `backend/windmill-native-triggers/src/google/` for the reference implementation.\n\n### Skipping update+get After Create (Google Pattern)\n\nOverride `service_config_from_create_response()` to return `Some(config)` when the external_id is known before the create call:\n\n```rust\nfn service_config_from_create_response(\n    &self,\n    data: &NativeTriggerData<Self::ServiceConfig>,\n    resp: &Self::CreateResponse,\n) -> Option<serde_json::Value> {\n    // Clone input config, add metadata from response\n    let mut config = data.service_config.clone();\n    config.google_resource_id = Some(resp.resource_id.clone());\n    config.expiration = Some(resp.expiration.clone());\n    Some(serde_json::to_value(&config).unwrap())\n}\n```\n\n### Services with Absolute OAuth Endpoints (Google)\n\nUnlike self-hosted services where OAuth endpoints are relative paths appended to `base_url`, services like Google have absolute URLs:\n\n```rust\n// Nextcloud: relative paths\nServiceName::Nextcloud => \"/apps/oauth2/api/v1/token\",\n// Google: absolute URLs\nServiceName::Google => \"https://oauth2.googleapis.com/token\",\n```\n\nThe `resolve_endpoint()` function handles both. For services with absolute endpoints:\n- `base_url` can be empty\n- `requiresBaseUrl: false` in the frontend workspace integration config\n- Add `extra_auth_params()` if needed (Google requires `access_type=offline` and `prompt=consent`)\n\n### Channel-Based Push Notifications with Renewal (Google Pattern)\n\nFor services using expiring watch channels instead of persistent webhooks:\n\n1. Store expiration in `service_config` (as part of `ServiceConfig`)\n2. In `maintain_triggers()`, implement renewal logic instead of using `reconcile_with_external_state()`:\n   ```rust\n   async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors) {\n       for trigger in triggers {\n           if should_renew_channel(trigger) {\n               self.renew_channel(db, trigger, oauth_data).await;\n           }\n       }\n   }\n   ```\n3. Renewal: best-effort stop old channel, create new one with same external_id, update service_config with new expiration\n4. Google example: Drive channels expire in 24h (renew when <1h left), Calendar channels expire in 7 days (renew when <1 day left)\n\n### reconcile_with_external_state (Nextcloud Pattern)\n\nThe reusable function in `sync.rs` compares external triggers with DB state:\n- Triggers missing externally: sets error \"Trigger no longer exists on external service\"\n- Triggers present externally: clears errors, updates service_config if it differs\n\nUsage in `maintain_triggers()`:\n```rust\nlet external_pairs: Vec<(String, serde_json::Value)> = /* fetch from external */;\ncrate::sync::reconcile_with_external_state(\n    db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n).await;\n```\n\n### Webhook Payload Processing\n\nOverride `prepare_webhook()` to parse service-specific payloads into script/flow args:\n\n```rust\nasync fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned> {\n    let mut args = HashMap::new();\n    args.insert(\"event_type\".to_string(), Box::new(headers.get(\"x-event-type\").cloned()) as _);\n    args.insert(\"payload\".to_string(), Box::new(serde_json::from_str::<serde_json::Value>(&body)?) as _);\n    Ok(PushArgsOwned { extra: None, args })\n}\n```\n\nThen register in `prepare_native_trigger_args()` in `lib.rs`:\n```rust\npub async fn prepare_native_trigger_args(service_name, db, w_id, headers, body) -> Result<Option<PushArgsOwned>> {\n    match service_name {\n        ServiceName::Google => { /* ... */ Ok(Some(args)) }\n        ServiceName::NewService => { /* ... */ Ok(Some(args)) }\n        ServiceName::Nextcloud => Ok(None), // Uses default body parsing\n    }\n}\n```\n\n### Instance-Level OAuth Credentials\n\nWhen `workspace_integrations.oauth_data.instance_shared == true`, `decrypt_oauth_data()` reads `client_id` and `client_secret` from instance-level global settings instead of workspace-level. This allows admins to share OAuth app credentials across workspaces.\n\n`generate_connect_url` picks the client (`resolve_oauth_client`): the workspace's own app when configured, the instance's otherwise. A GitHub OAuth app only redirects below its single registered callback, so connections made with the instance GitHub app land on `/oauth/callback/github/native_trigger` instead of the workspace settings page.\n\n---\n\n## Testing Checklist\n\n- [ ] Database migration runs successfully\n- [ ] `cargo check -p windmill-native-triggers --features native_trigger` passes\n- [ ] `npx svelte-check --threshold error` passes (in frontend/)\n- [ ] Service appears in workspace integrations list\n- [ ] OAuth flow completes successfully\n- [ ] Can create a new trigger\n- [ ] Can view trigger details\n- [ ] Can update trigger configuration\n- [ ] Can delete trigger\n- [ ] Webhook receives and processes payloads\n- [ ] Background sync works correctly (reconciliation or channel renewal)\n- [ ] Error handling works (expired tokens, service unavailable)\n\n---\n\n## Reference Implementations\n\n### Nextcloud (Self-Hosted, Update+Get Pattern)\n\n| File | Purpose |\n|------|---------|\n| `nextcloud/mod.rs` | Types: NextCloudOAuthData, NextcloudServiceConfig, NextCloudTriggerData |\n| `nextcloud/external.rs` | External trait: uses update+get pattern, reconcile_with_external_state for sync |\n| `nextcloud/routes.rs` | Additional route: `GET /events` |\n\nKey patterns: relative OAuth endpoints, base_url required, list_all + reconcile for sync, update returns JSON from get().\n\n### Google (Cloud, Unified Service, Short Create)\n\n| File | Purpose |\n|------|---------|\n| `google/mod.rs` | Types: GoogleServiceConfig with trigger_type discriminator, GoogleTriggerType enum |\n| `google/external.rs` | External trait: overrides service_config_from_create_response, channel renewal for sync |\n| `google/routes.rs` | Additional routes: `GET /calendars`, `GET /drive/files`, `GET /drive/shared_drives` |\n\nKey patterns: absolute OAuth endpoints, empty base_url, trigger_type for Drive/Calendar, expiring watch channels with renewal, service_config_from_create_response skips update+get, get() reconstructs data from stored service_config (no external \"get channel\" API).\n",".agents/skills/grilling/SKILL.md":"---\nname: grilling\ndescription: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n---\n\nInterview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.\n\nWork the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.\n\nEach question should be formatted like so:\n\n```\n❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>\n\n➡️ <your recommended answer>\n```\n\nEach round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.\n\nFinding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it — don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report — ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait.\n\nThe session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.\n",".agents/skills/pr/SKILL.md":"# pr\n\nPath: `.agents/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/pr/SKILL.md)",".agents/skills/refine/SKILL.md":"# refine\n\nPath: `.agents/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/refine/SKILL.md)",".agents/skills/rust-backend/SKILL.md":"# rust-backend\n\nPath: `.agents/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/rust-backend/SKILL.md)",".agents/skills/svelte-frontend/SKILL.md":"# svelte-frontend\n\nPath: `.agents/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/svelte-frontend/SKILL.md)",".agents/skills/update-sqlx/SKILL.md":"# update-sqlx\n\nPath: `.agents/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/update-sqlx/SKILL.md)",".claude/skills/adding-a-trigger/SKILL.md":"# adding-a-trigger\n\nPath: `.claude/skills/adding-a-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/adding-a-trigger/SKILL.md)",".claude/skills/ai-chat/SKILL.md":"# ai-chat\n\nPath: `.claude/skills/ai-chat/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-chat/SKILL.md)",".claude/skills/ai-evals/SKILL.md":"# ai-evals\n\nPath: `.claude/skills/ai-evals/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-evals/SKILL.md)",".claude/skills/codebase-design/SKILL.md":"# codebase-design\n\nPath: `.claude/skills/codebase-design/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/codebase-design/SKILL.md)",".claude/skills/commit/SKILL.md":"# commit\n\nPath: `.claude/skills/commit/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/commit/SKILL.md)",".claude/skills/domain-modeling/SKILL.md":"# domain-modeling\n\nPath: `.claude/skills/domain-modeling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/domain-modeling/SKILL.md)",".claude/skills/grill-me/SKILL.md":"# grill-me\n\nPath: `.claude/skills/grill-me/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grill-me/SKILL.md)",".claude/skills/grilling/SKILL.md":"# grilling\n\nPath: `.claude/skills/grilling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grilling/SKILL.md)",".claude/skills/improve-codebase-architecture/SKILL.md":"# improve-codebase-architecture\n\nPath: `.claude/skills/improve-codebase-architecture/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/improve-codebase-architecture/SKILL.md)",".claude/skills/local-review-codex/SKILL.md":"# local-review-codex\n\nPath: `.claude/skills/local-review-codex/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review-codex/SKILL.md)",".claude/skills/local-review/SKILL.md":"# local-review\n\nPath: `.claude/skills/local-review/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review/SKILL.md)",".claude/skills/native-trigger/SKILL.md":"# native-trigger\n\nPath: `.claude/skills/native-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/native-trigger/SKILL.md)",".claude/skills/pr/SKILL.md":"# pr\n\nPath: `.claude/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/pr/SKILL.md)",".claude/skills/refine/SKILL.md":"# refine\n\nPath: `.claude/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/refine/SKILL.md)",".claude/skills/rust-backend/SKILL.md":"# rust-backend\n\nPath: `.claude/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/rust-backend/SKILL.md)",".claude/skills/svelte-frontend/SKILL.md":"# svelte-frontend\n\nPath: `.claude/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/svelte-frontend/SKILL.md)",".claude/skills/update-sqlx/SKILL.md":"# update-sqlx\n\nPath: `.claude/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/update-sqlx/SKILL.md)","system_prompts/auto-generated/skills/cli-commands/SKILL.md":"# cli-commands\n\nPath: `system_prompts/auto-generated/skills/cli-commands/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/cli-commands/SKILL.md)","system_prompts/auto-generated/skills/datatables/SKILL.md":"# datatables\n\nPath: `system_prompts/auto-generated/skills/datatables/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/datatables/SKILL.md)","system_prompts/auto-generated/skills/preview/SKILL.md":"# preview\n\nPath: `system_prompts/auto-generated/skills/preview/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/preview/SKILL.md)","system_prompts/auto-generated/skills/raw-app/SKILL.md":"# raw-app\n\nPath: `system_prompts/auto-generated/skills/raw-app/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/raw-app/SKILL.md)","system_prompts/auto-generated/skills/resources/SKILL.md":"# resources\n\nPath: `system_prompts/auto-generated/skills/resources/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/resources/SKILL.md)","system_prompts/auto-generated/skills/schedules/SKILL.md":"# schedules\n\nPath: `system_prompts/auto-generated/skills/schedules/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/schedules/SKILL.md)","system_prompts/auto-generated/skills/triggers/SKILL.md":"# triggers\n\nPath: `system_prompts/auto-generated/skills/triggers/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/triggers/SKILL.md)","system_prompts/auto-generated/skills/workspace-settings/SKILL.md":"# workspace-settings\n\nPath: `system_prompts/auto-generated/skills/workspace-settings/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/workspace-settings/SKILL.md)","system_prompts/auto-generated/skills/write-flow/SKILL.md":"# write-flow\n\nPath: `system_prompts/auto-generated/skills/write-flow/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-flow/SKILL.md)","system_prompts/auto-generated/skills/write-pipeline/SKILL.md":"# write-pipeline\n\nPath: `system_prompts/auto-generated/skills/write-pipeline/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-pipeline/SKILL.md)","system_prompts/auto-generated/skills/write-script-ansible/SKILL.md":"# write-script-ansible\n\nPath: `system_prompts/auto-generated/skills/write-script-ansible/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-ansible/SKILL.md)","system_prompts/auto-generated/skills/write-script-bash/SKILL.md":"# write-script-bash\n\nPath: `system_prompts/auto-generated/skills/write-script-bash/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bash/SKILL.md)","system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md":"# write-script-bigquery\n\nPath: `system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md)","system_prompts/auto-generated/skills/write-script-bun/SKILL.md":"# write-script-bun\n\nPath: `system_prompts/auto-generated/skills/write-script-bun/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bun/SKILL.md)","system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md":"# write-script-bunnative\n\nPath: `system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md)","system_prompts/auto-generated/skills/write-script-csharp/SKILL.md":"# write-script-csharp\n\nPath: `system_prompts/auto-generated/skills/write-script-csharp/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md)","system_prompts/auto-generated/skills/write-script-deno/SKILL.md":"# write-script-deno\n\nPath: `system_prompts/auto-generated/skills/write-script-deno/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-deno/SKILL.md)","system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md":"# write-script-duckdb\n\nPath: `system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md)","system_prompts/auto-generated/skills/write-script-go/SKILL.md":"# write-script-go\n\nPath: `system_prompts/auto-generated/skills/write-script-go/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-go/SKILL.md)","system_prompts/auto-generated/skills/write-script-graphql/SKILL.md":"# write-script-graphql\n\nPath: `system_prompts/auto-generated/skills/write-script-graphql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md)","system_prompts/auto-generated/skills/write-script-java/SKILL.md":"# write-script-java\n\nPath: `system_prompts/auto-generated/skills/write-script-java/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-java/SKILL.md)","system_prompts/auto-generated/skills/write-script-mssql/SKILL.md":"# write-script-mssql\n\nPath: `system_prompts/auto-generated/skills/write-script-mssql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md)","system_prompts/auto-generated/skills/write-script-mysql/SKILL.md":"# write-script-mysql\n\nPath: `system_prompts/auto-generated/skills/write-script-mysql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md)","system_prompts/auto-generated/skills/write-script-php/SKILL.md":"# write-script-php\n\nPath: `system_prompts/auto-generated/skills/write-script-php/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-php/SKILL.md)","system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md":"# write-script-postgresql\n\nPath: `system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md)","system_prompts/auto-generated/skills/write-script-powershell/SKILL.md":"# write-script-powershell\n\nPath: `system_prompts/auto-generated/skills/write-script-powershell/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md)","system_prompts/auto-generated/skills/write-script-python3/SKILL.md":"# write-script-python3\n\nPath: `system_prompts/auto-generated/skills/write-script-python3/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-python3/SKILL.md)","system_prompts/auto-generated/skills/write-script-rlang/SKILL.md":"# write-script-rlang\n\nPath: `system_prompts/auto-generated/skills/write-script-rlang/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md)","system_prompts/auto-generated/skills/write-script-rust/SKILL.md":"# write-script-rust\n\nPath: `system_prompts/auto-generated/skills/write-script-rust/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rust/SKILL.md)","system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md":"# write-script-snowflake\n\nPath: `system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md)","system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md":"# write-workflow-as-code\n\nPath: `system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/CLAUDE.md)",".codex/config.toml":"# Codex Directive: config.toml\n\nPath: `.codex/config.toml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.codex/config.toml)","ai_evals/AGENTS.md":"# ai_evals - AI Agent Protocol & Instructions\n\nPath: `ai_evals/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/AGENTS.md)","ai_evals/CLAUDE.md":"# ai_evals - Claude Agent Guidelines & System Prompt\n\nPath: `ai_evals/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/CLAUDE.md)","backend/AGENTS.md":"# backend - AI Agent Protocol & Instructions\n\nPath: `backend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/AGENTS.md)","backend/CLAUDE.md":"# backend - Claude Agent Guidelines & System Prompt\n\nPath: `backend/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/CLAUDE.md)","frontend/AGENTS.md":"# frontend - AI Agent Protocol & Instructions\n\nPath: `frontend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/AGENTS.md)","frontend/CLAUDE.md":"# frontend - Claude Agent Guidelines & System Prompt\n\nPath: `frontend/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/CLAUDE.md)",".agents/skills/UPSTREAM.md":"# Skills Skill\n\nPath: `.agents/skills/UPSTREAM.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/UPSTREAM.md)",".claude/agents/branch-diff-reviewer.md":"# Subagent: branch-diff-reviewer\n\nPath: `.claude/agents/branch-diff-reviewer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/branch-diff-reviewer.md)",".claude/agents/openapi-sync.md":"# Subagent: openapi-sync\n\nPath: `.claude/agents/openapi-sync.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/openapi-sync.md)","backend/generate_mcp_endpoints_tools/README.md":"# generate_mcp_endpoints_tools Documentation\n\nPath: `backend/generate_mcp_endpoints_tools/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/generate_mcp_endpoints_tools/README.md)",".agents/skills/codebase-design/DEEPENING.md":"# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DEEPENING.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DEEPENING.md)",".agents/skills/codebase-design/DESIGN-IT-TWICE.md":"# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DESIGN-IT-TWICE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DESIGN-IT-TWICE.md)",".agents/skills/domain-modeling/CONTEXT-FORMAT.md":"# Domain-modeling Skill\n\nPath: `.agents/skills/domain-modeling/CONTEXT-FORMAT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/domain-modeling/CONTEXT-FORMAT.md)",".agents/skills/improve-codebase-architecture/HTML-REPORT.md":"# Improve-codebase-architecture Skill\n\nPath: `.agents/skills/improve-codebase-architecture/HTML-REPORT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/improve-codebase-architecture/HTML-REPORT.md)","ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml)","ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml)","ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml":"# Reuse_greeting__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml)","ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/AGENTS.md)","tokens":300,"sizeBytes":156},{"name":"AGENTS.md","path":"ai_evals/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/AGENTS.md","title":"ai_evals - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# ai_evals - AI Agent Protocol & Instructions\n\nPath: `ai_evals/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/AGENTS.md)","tokens":300,"sizeBytes":185},{"name":"AGENTS.md","path":"backend/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/AGENTS.md","title":"backend - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# backend - AI Agent Protocol & Instructions\n\nPath: `backend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/AGENTS.md)","tokens":300,"sizeBytes":182},{"name":"AGENTS.md","path":"frontend/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/AGENTS.md","title":"frontend - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# frontend - AI Agent Protocol & Instructions\n\nPath: `frontend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/AGENTS.md)","tokens":300,"sizeBytes":185},{"name":"SKILL.md","path":".agents/skills/adding-a-trigger/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/adding-a-trigger/SKILL.md","title":"adding-a-trigger","category":"anthropic-skill","format":"markdown","content":"---\nname: adding-a-trigger\ndescription: 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.\n---\n\n# Skill: Adding a New Trigger Type\n\nUse 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.\n\nThe 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.\n\nThroughout this doc, substitute `{kind}` for the new trigger kind (`azure`, `kafka`, …), `{Kind}` for PascalCase (`Azure`, `Kafka`), `{KIND}` for SCREAMING (`AZURE`, `KAFKA`).\n\n## Reference implementations\n\n- **GCP** — closest analogue to Azure. Has push + pull, OIDC auth, ARM-like resource paths, capture handler. Grep for `gcp_trigger` / `GcpTrigger`.\n- **Kafka** — simpler (pull-only, streaming). Good for trivial integrations.\n- **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`.\n\n## 1. Database migration\n\nCreate a migration: `cargo sqlx migrate add -r add_{kind}_trigger` from `backend/`. Never write timestamps manually.\n\nThe `up.sql` usually defines:\n- An optional enum type (e.g. `AZURE_MODE`) if the trigger has sub-kinds\n- The `{kind}_trigger` table with at minimum these columns (mirrored from kafka/gcp):\n  - primary: `(workspace_id, path)`\n  - `script_path`, `is_flow`, `enabled`, `mode`, `permissioned_as`, `edited_by`, `email`\n  - `edited_at`, `error`, `server_id`, `last_server_ping`\n  - `error_handler_path`, `error_handler_args jsonb`, `retry jsonb`\n  - trigger-specific fields\n- Indexes on foreign keys + any frequently-filtered columns\n- Foreign key to `workspace`\n- The RLS policies (`see_own`, `see_member`, `see_folder_extra_perms_user_*`, `see_extra_perms_user_*`, `see_extra_perms_groups_*`), copied from an existing trigger table\n\n**RLS: wrap every session GUC read in a scalar sub-select.** Write the session\nreads as `(select current_setting('session.user'))`,\n`= any((select regexp_split_to_array(current_setting('session.groups'), ','))::text[])`,\n`?| (select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[]`,\n`? (select concat('u/', current_setting('session.user')))`, etc. — not the bare\n`current_setting(...)`. The GUCs are set with `SET LOCAL`, so the sub-select\nhoists them to a one-time InitPlan instead of re-evaluating per scanned row.\nPut the `::text[]` cast **outside** the sub-select for the array cases: in an\n`= any (...)` context, casting inside — `= any((select ...::text[]))` — makes\nPostgres parse the operand as a row-returning subquery and fails at CREATE with\n`operator does not exist: text = text[]`. The outside cast keeps it in\narray-operand form. See migration `20260714230440_wrap_session_gucs_in_rls_policies`\nfor the canonical wrapped forms.\n\nDown migration drops the table and any enum types.\n\n## 2. Backend crate (`windmill-trigger-{kind}`)\n\nCreate a new crate under `backend/windmill-trigger-{kind}/` with:\n\n- `Cargo.toml`: features `enterprise`, `private` if EE, standard deps\n- `src/lib.rs`: `pub use mod_ee::*;` behind `#[cfg(all(feature = \"enterprise\", feature = \"private\"))]`\n- `src/mod_ee.rs`: core types + helpers\n- `src/handler_ee.rs`: `TriggerCrud` impl + route handlers\n- `src/listener_ee.rs`: (only if streaming/pull-based) `Listener` trait impl\n\nRequired in `mod_ee.rs`:\n- `{Kind}Config` struct (persisted shape, `FromRow`)\n- `{Kind}ConfigRequest` struct (what API receives — usually similar to Config but with validation fields)\n- `{Kind}Trigger` unit struct (implements the traits)\n- `impl TriggerJobArgs for {Kind}Trigger` — sets `TRIGGER_KIND`, `Payload`, `v1_payload_fn`\n\nRequired in `handler_ee.rs`:\n- `#[async_trait] impl TriggerCrud for {Kind}Trigger` with:\n  - `type Trigger = Trigger<{Kind}Config>`\n  - `type TriggerConfigRequest = {Kind}ConfigRequest`\n  - `const ROUTE_PREFIX: &'static str = \"/{kind}_triggers\";`\n  - `const TABLE_NAME`, `ADDITIONAL_SELECT_FIELDS`\n  - `get_deployed_object`, `validate_config`, `create_trigger`, `update_trigger`, `delete_trigger`, `test_connection`\n  - `additional_routes` (optional — mount extra endpoints for things like ARM resource listing, topic discovery)\n\nRegister the crate in `backend/Cargo.toml` as a workspace member and as a dep of `windmill-api` behind the feature flag.\n\n## 3. Wire into `windmill-api` (feature-gated everywhere)\n\n**`backend/windmill-api/src/triggers/handler.rs`** — mount the trigger crate:\n```rust\n#[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n{\n    use crate::triggers::{kind}::{Kind}Trigger;\n    router = router.nest({Kind}Trigger::ROUTE_PREFIX, complete_trigger_routes({Kind}Trigger));\n}\n```\n\n**`backend/windmill-api/src/triggers/{kind}/mod.rs`** — re-export the crate:\n```rust\npub use windmill_trigger_{kind}::*;\n```\n\n**`backend/windmill-api/src/lib.rs`** — if the trigger receives inbound pushes, add a webhook route:\n```rust\n.nest(\"/{kind}/w/{workspace_id}\", {\n    #[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n    { triggers::{kind}::handler_oss::{kind}_push_route_handler() }\n    #[cfg(not(...))]\n    { Router::new() }\n})\n```\n\n## 4. `TriggerKind` enum (`backend/windmill-types/src/triggers.rs`)\n\nAlready has slots for most triggers but verify your variant exists:\n- Add `{Kind}` to the `TriggerKind` enum\n- Add match arm in `to_key()`\n- Add match arm in `from_str`\n- Add match arm in `JobTriggerKind` (if jobs need kind tagging)\n\n## 5. OpenAPI (`backend/windmill-api/openapi.yaml`)\n\nThis file is huge and the single most-forgotten place. Add:\n\n- `/w/{workspace}/{kind}_triggers/create` + `/update/{path}` + `/delete/{path}` + `/get/{path}` + `/list` + `/exists/{path}` + `/setmode/{path}` + `/test` paths (mirror gcp section)\n- Any `additional_routes` your handler exposes (resource discovery, etc.)\n- Schemas: `{Kind}Trigger`, `{Kind}TriggerData`, `{Kind}Mode` (if enum), `{Kind}DeliveryConfig`, helper request/response types\n- Add `{kind}` to `CaptureTriggerKind` enum\n- Add `{kind}_used: boolean` to the `UsedTriggers` response schema\n\nRegenerate frontend client: `npm run generate-backend-client` from `frontend/`.\n\n## 6. `UsedTriggers` + workspace export\n\n**`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.\n\n**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON). The block re-uses the `trigger_ignore_keys` variable so the new kind automatically participates in fork-export stripping (`mode` field is omitted when the source workspace is a fork — keeps fork→parent merges from flipping the parent's enabled state).\n\n**Fork cloning (`clone_triggers_and_schedules` in workspaces.rs)** — add an `INSERT INTO {kind}_trigger ... SELECT ...` block that copies all rows from the parent workspace, forcing `mode = 'disabled'::TRIGGER_MODE`. Always runs at fork creation; forgetting this means users can't carry `{kind}` triggers into their forks.\n\n## 6.5 Hardcoded trigger-kind arrays (silent-failure hotspots)\n\nSeveral files keep **hardcoded arrays** of trigger kind strings. Miss one and ACL checks / user offboarding / trash drop your kind:\n\n- **`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.\n- **`backend/windmill-api-users/src/users.rs`** (`extra_perms_tables`) — which tables get `extra_perms` entries cleaned when a user is deleted.\n- **`backend/windmill-api/src/offboarding.rs`** — three separate arrays (enumeration, fork-copy, and delete paths). **All three** need the new kind.\n- **`backend/windmill-api/src/trash.rs`** — `valid_tables` for the trash / restore API.\n- **`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).\n- **`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.\n- **`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.\n\n**OpenAPI enums** to extend (do NOT forget — generated client will allow it but server rejects as 400):\n- `CaptureTriggerKind` enum\n- Three `kind` enums under `/w/{workspace}/acls/{get,add,remove}/{kind}/{path}` (yes, same list repeated three times)\n\nAfter 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.\n\n## 7. Capture infrastructure (`backend/windmill-api/src/capture.rs`)\n\nIf the trigger supports push delivery, it also needs a capture endpoint so users can test it:\n\n- `{Kind}TriggerConfig` struct (gated by feature flags)\n- `TriggerConfig::{Kind}` variant\n- `set_{kind}_trigger_config` function (creates the subscription/equivalent pointing at the capture URL — use your `manage_{kind}_subscription` helper with `trigger_mode=false`)\n- Both real + no-op versions behind feature gates\n- `TriggerKind::{Kind} => set_{kind}_trigger_config(...)` arm in `set_config`\n- `{kind}_payload` async handler — validates auth (if any), processes payload, calls `insert_capture_payload`\n- Route: `.route(\"/{kind}/{runnable_kind}/{*path}\", post({kind}_payload))` inside `workspaced_unauthed_service` — and expand the surrounding `#[cfg(any(...))]` to include your feature flag\n\n## 8. CLI (`cli/`) — easy to miss, breaks sync silently\n\nCheck all of these:\n\n**`cli/src/types.ts`:**\n- Add `\"{kind}\"` to `TRIGGER_TYPES` array\n- Add `\"{kind}_trigger\"` to `getTypeStrFromPath` return union\n- Add match case in `getTypeStrFromPath`'s `typeEnding ===` chain\n- Add `pushTrigger(\"{kind}\", ...)` branch in `pushObj`\n\n**`cli/src/commands/trigger/trigger.ts`:**\n- Import `{Kind}Trigger` type\n- Add `{kind}: {Kind}Trigger` to the `Trigger` type map\n- Add `{kind}: wmill.get{Kind}Trigger`, `update{Kind}Trigger`, `create{Kind}Trigger` to each function map\n- Add `{kind}: { ... }` template to `triggerTemplates`\n- Add `list{Kind}Triggers` call + spread in the `list` aggregation\n- Update `--kind` option descriptions to mention the new kind\n\n**`cli/src/commands/sync/sync.ts`:**\n- Add `path.endsWith(\".{kind}_trigger\" + ext)` in the file-type filter\n- Add `typ == \"{kind}_trigger\"` in `getTypeOrder`\n- Add `\"{kind}_trigger\"` to the delete-suffix regex (~line 3092)\n- Add a `case \"{kind}_trigger\"` in the delete switch\n\n**`cli/src/guidance/skills.ts`** — **DO NOT EDIT DIRECTLY**. It's auto-generated by `system_prompts/generate.py`. Instead:\n- 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)\n- Then run `python3 system_prompts/generate.py` — it regenerates `cli/src/guidance/skills.ts` with the schema extracted from `backend/windmill-api/openapi.yaml`\n- Commit the regenerated file\n\n## 9. Frontend — editor + drawer\n\nUnder `frontend/src/lib/components/triggers/{kind}/`:\n\n- `{Kind}TriggerPanel.svelte` — the tile shown in the triggers listing\n- `{Kind}TriggerEditor.svelte` — outer drawer wrapper\n- `{Kind}TriggerEditorInner.svelte` — state + business logic; must expose:\n  - `openEdit(path, isFlow, defaultValues?)` method\n  - `isEditor` prop, `onConfigChange` + `onCaptureConfigChange` callbacks\n  - `get{Kind}Config()` + `get{Kind}CaptureConfig()` helpers\n  - `captureConfig = $derived.by(untrack(() => isEditor) ? get{Kind}CaptureConfig : () => ({}))`\n  - `$effect(() => { const args = [captureConfig, isValid] as const; untrack(() => onCaptureConfigChange?.(...args)) })`\n- `{Kind}TriggerEditorConfigSection.svelte` — form fields; use design-system components (`TextInput`, `Select`, `Toggle`, `ToggleButtonGroup`), never raw `<input>`\n- `{Kind}Capture.svelte` — capture panel; wraps `CaptureSection` with `captureType=\"{kind}\"`\n- `utils.ts` — `requestBody` builders and any trigger-type-specific helpers\n\n## 10. Frontend — global integration\n\nEasy to miss:\n\n- **`frontend/src/lib/components/triggers.ts`** — add `'{kind}'` to the `TriggerKind` union\n- **`frontend/src/lib/components/triggers/CaptureWrapper.svelte`**:\n  - Import `{Kind}Capture`\n  - Add to `isStreamingCapture()` array (streaming = pull-style; push-style is typically `false`)\n  - Add `{:else if captureType === '{kind}'}` branch with the `<{Kind}Capture>` render\n- **`frontend/src/lib/components/sidebar/SidebarContent.svelte`** — import the icon, add the nav entry\n- **`frontend/src/lib/components/sidebar/OperatorMenu.svelte`** — add the operator-mode entry\n- **`frontend/src/routes/(root)/(logged)/+layout.svelte`** — destructure `{kind}_used` from `/get_used_triggers` response, push `'{kind}'` into `usedKinds`\n- **`frontend/src/lib/components/search/GlobalSearchModal.svelte`** — import icon, add \"Go to {Kind} ...\" entry\n- **`frontend/src/lib/components/offboarding-utils.ts`** — add mappings `{kind}_trigger: '{kind}_triggers'` and `{kind}_trigger: '{kind} trigger'`\n- **`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)\n- **`frontend/src/routes/(root)/(logged)/{kind}_triggers/+page.svelte`** — listing page (mirror `gcp_triggers/+page.svelte` for push+pull, `kafka_triggers` for pure streaming)\n- **`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\n\n## 10.5 AI system prompts (`system_prompts/`)\n\n- **`system_prompts/utils.py`** — append `('{Kind}Trigger', '{kind}_trigger')` to `SCHEMA_MAPPINGS['triggers']` (master list used by code generation + CLI skills)\n- **`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\n- **`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)\n- Run `python3 system_prompts/generate.py` — this rewrites `cli/src/guidance/skills.ts` and all `auto-generated/` docs. Commit the regenerated files\n\n## 11. Validation\n\nRun all of these before declaring done:\n\n```bash\n# Backend\ncd backend\ncargo check --features enterprise,{kind}_trigger,private   # minimal\ncargo check --features enterprise,azure_trigger,private,gcp_trigger,http_trigger,mqtt_trigger,postgres_trigger,sqs_trigger,kafka,nats,smtp,websocket  # full\n\n# SQLx offline data (never run `cargo sqlx prepare` directly — use the wrapper)\n./update_sqlx.sh\n\n# Frontend\ncd frontend\nnpm run generate-backend-client\nnpm run check:fast\n```\n\nSmoke test in the UI: create a trigger, save, check it appears in sidebar + search, delete, re-create via CLI `wmill sync`.\n\n## 12. Common pitfalls\n\n- **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\n- **`.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`\n- **`on:event` directives** — legacy Svelte 4, no-op in runes mode. Use callback props (`onSelected`, `onConfigChange`)\n- **`$bindable(default_value)` on optional props** — banned by project CLAUDE.md. Use `$bindable()` + `$derived(prop ?? default)` instead\n- **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)\n- **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\n- **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\n- **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\n- **Workspace-scoped tag compatibility** — if the trigger has tags, verify forked workspaces handle them (see commit `0773b5bc85` for a historical fix)\n\n## 13. EE file split\n\nIf 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.\n\n## 14. Final checklist before PR\n\n- [ ] Migration up/down tested (revert + re-apply)\n- [ ] `./update_sqlx.sh` committed the updated `.sqlx/` offline data\n- [ ] `cargo check` passes with your feature flag + with all trigger features\n- [ ] `npm run check:fast` passes\n- [ ] Trigger visible in sidebar with correct icon weight (not oversized/colored — use `currentColor`)\n- [ ] Create, edit, delete flow all work in the UI\n- [ ] Capture button works (if push-capable)\n- [ ] Trigger appears in `/get_used_triggers` → sidebar pulse\n- [ ] `wmill sync pull` + `wmill sync push` both round-trip the trigger\n- [ ] `wmill trigger list` includes it\n- [ ] OpenAPI schemas are complete (no `null` in generated types)\n","frontmatter":{"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."},"isInternal":false,"tokens":4901,"sizeBytes":18932},{"name":"SKILL.md","path":".agents/skills/ai-chat/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/ai-chat/SKILL.md","title":"ai-chat","category":"anthropic-skill","format":"markdown","content":"---\nname: ai-chat\ndescription: Guidance for improving the Windmill AI chat (copilot), especially global mode — tools, prompts, and context-window discipline. Use when editing chat tools, system prompts, or tool-result shapes under frontend/src/lib/components/copilot/chat, or when changing how the chat manages its context window.\n---\n\n## Always benchmark before and after\n\nNo context or behavior change ships without an `ai_evals` A/B on the affected mode.\nAdd or adjust cases for exactly what you changed — see the `ai-evals` skill for\nauthoring and the full run reference.\n\nRun the affected mode **before** your change and **after**, same model(s), same cases.\n\n## Where guidance goes\n\nWhat the model should know about Windmill itself (flow shapes, groups, data tables, app\naccess, secrets…) belongs in `system_prompts/base/` or `system_prompts/languages/`, which also\nfeed the CLI's skills — not in a `*/core.ts` prompt builder, where only this chat would see it.\nThe builders add tool plumbing and runtime values only. Scope a sentence to one consumer with\n`<!-- chat-only -->` / `<!-- cli-only -->`, keep chat tool names out of the shared files, and\nrerun `python system_prompts/generate.py`. `system_prompts/README.md` has the mechanics.\n\n## Measure the window first, and cumulative second\n\nOptimize **`finalContextTokens`** (window occupancy — what drives overflow and\ncompaction), then cumulative prompt tokens.\n\n## Context discipline\n\nThe dominant fixed cost is per-iteration overhead: the system prompt **plus every\ntool schema** is re-sent on every loop iteration. So:\n\n- **Every tool and every parameter is a permanent tax.** Justify each one and measure\n  it; an extra \"locate\" round-trip can cost more than the reads it saves. Strip dead\n  params rather than leaving them in the schema.\n- **Tool results return the minimum.** Never echo content the model already has. The\n  canonical mistake: a write tool that returns the whole edited artifact right after\n  the model authored it — return `{ success, message }` instead. When you touch a\n  *shared* write helper (e.g. `finishAppDraftWrite` in `global/core.ts`), re-check\n  this invariant for **all** the write tools routing through it — the echo has\n  regressed before via a shared refactor.\n\n## Prompts and tool descriptions are part of the surface\n\nThe system prompt and tool descriptions steer behavior as much as the tools\nthemselves, and are benchmarkable the same way. A description that advertises\ntruncation makes the model self-limit; the path-conventions block changes where\ndrafts land. Treat prompt/description edits as real changes and A/B them — a\npure-prompt change is a legitimate, measurable improvement.","frontmatter":{"name":"ai-chat","description":"Guidance for improving the Windmill AI chat (copilot), especially global mode — tools, prompts, and context-window discipline. Use when editing chat tools, system prompts, or tool-result shapes under frontend/src/lib/components/copilot/chat, or when changing how the chat manages its context window."},"isInternal":false,"tokens":606,"sizeBytes":2703},{"name":"SKILL.md","path":".agents/skills/ai-evals/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/ai-evals/SKILL.md","title":"ai-evals","category":"anthropic-skill","format":"markdown","content":"---\nname: ai-evals\ndescription: Author and run black-box benchmark cases for the Windmill AI generation modes (flow/app/script/cli/global) in ai_evals/. Use when adding or changing eval cases, or when running before/after benchmarks for AI chat / copilot changes.\n---\n\n# AI evals — authoring and running benchmark cases\n\n`ai_evals/` is a black-box benchmark runner for the Windmill AI generation modes:\n`flow`, `app`, `script`, `cli`, `global`. It always tests the **current** production\nprompts, tools, and guidance in this checkout. Each attempt runs the real production\npath, deterministic validation, then LLM judging.\n\nThe goal is to test current production guidance with realistic user requests — **not**\nto pin one exact implementation shape.\n\n## Running benchmarks\n\n```bash\ncd ai_evals\nbun install                       # first time; frontend modes also need `cd frontend && bun install`\nbun run cli -- models             # list model aliases\nbun run cli -- cases global       # list cases for a mode\nbun run cli -- run global global-test1-script-create --model sonnet\n```\n\nFrontend modes (`flow`/`script`/`app`/`global`) route model calls through a Windmill\nbackend's `/api/w/<ws>/ai/proxy`, so you need **any** reachable backend:\n\n```bash\nWMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:<port> WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests \\\n  bun run cli -- run global <caseIds...> --models sonnet,gpt-5.5,gemini-3.1-pro-preview\n```\n\n- **Reuse an existing workspace.** CE builds cap workspaces, so temp-workspace\n  creation 400s (\"reached workspace limit\"). Always set\n  `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` (or any existing workspace) to\n  reuse one. The only side effect of a run is upserting an `f/evals/ai/<provider>`\n  resource there.\n- Provider keys live in `ai_evals/.env` and are auto-loaded by bun. The judge is a\n  separate Anthropic call (default `claude-sonnet-4-6`) regardless of the model under\n  test.\n\n## Authoring core rules\n\n1. Write prompts like a real user request.\n2. Prefer behavior, inputs, constraints, and outcomes over internal implementation.\n3. Keep deterministic validation narrow and hard.\n4. Put semantic expectations in `judgeChecklist`.\n5. Use `expected` fixtures only when exact structure really matters.\n\n### Prompt writing\n\nPrompts should sound like something a user would naturally ask. Do not write prompts\nas if the user knows Windmill internals unless the case explicitly tests a power-user\nworkflow.\n\nGood:\n- \"Create a flow that routes support requests based on customer tier.\"\n- \"Add a reset button that sets the counter back to 0.\"\n- \"Create a flow that reuses the existing greeting script instead of duplicating the logic.\"\n\nBad:\n- \"Use `branchone` with 3 branches and a default branch.\"\n- \"Create a `rawscript` step with this exact topology.\"\n- \"This is a benchmark harness.\"\n\n### Deterministic validation\n\nUse deterministic checks only for hard failures: missing required files; unexpected\nextra files when the prompt says not to create them; syntax errors; unresolved flow\nrefs; missing required special modules or suspend config; obvious corruption.\n\nDo **not** encode one preferred implementation. Bad hard checks: exact step topology\nfor a creation flow; exact branch structure when the prompt only asked for routing;\nexact input shape when multiple reasonable shapes are acceptable.\n\n### Judge checklist\n\nEvery non-trivial case should have a `judgeChecklist` capturing user-visible behavior\nthat must be present, important constraints, and key completion criteria — not\nlow-level implementation details unless truly required.\n\nGood: \"the flow calculates the order total with 8% tax\"; \"the flow reuses the existing\nworkspace script instead of rewriting the logic\". Bad: \"uses `branchone`\"; \"contains a\n`rawscript` node\".\n\nSee `ai_evals/README.md` for the full case format, fields, and fixture details.","frontmatter":{"name":"ai-evals","description":"Author and run black-box benchmark cases for the Windmill AI generation modes (flow/app/script/cli/global) in ai_evals/. Use when adding or changing eval cases, or when running before/after benchmarks for AI chat / copilot changes."},"isInternal":false,"tokens":894,"sizeBytes":3882},{"name":"DEEPENING.md","path":".agents/skills/codebase-design/DEEPENING.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DEEPENING.md","title":"Codebase-design Skill","category":"anthropic-skill","format":"markdown","content":"# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DEEPENING.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DEEPENING.md)","tokens":300,"sizeBytes":213},{"name":"DESIGN-IT-TWICE.md","path":".agents/skills/codebase-design/DESIGN-IT-TWICE.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DESIGN-IT-TWICE.md","title":"Codebase-design Skill","category":"anthropic-skill","format":"markdown","content":"# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DESIGN-IT-TWICE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DESIGN-IT-TWICE.md)","tokens":300,"sizeBytes":225},{"name":"SKILL.md","path":".agents/skills/codebase-design/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/SKILL.md","title":"codebase-design","category":"anthropic-skill","format":"markdown","content":"---\nname: codebase-design\ndescription: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\n---\n\n# Codebase Design\n\nDesign **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone.\n\n## Glossary\n\nUse these terms exactly — don't substitute \"component,\" \"service,\" \"API,\" or \"boundary.\" Consistent language is the whole point.\n\n**Module** — anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.\n\n**Interface** — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow — they refer only to the type-level surface).\n\n**Implementation** — what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for \"adapter\" when the seam is the topic; \"implementation\" otherwise.\n\n**Depth** — leverage at the interface: the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.\n\n**Seam** _(Michael Feathers)_ — a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).\n\n**Adapter** — a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).\n\n**Leverage** — what callers get from depth: more capability per unit of interface they learn. One implementation pays back across N call sites and M tests.\n\n**Locality** — what maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.\n\n## Deep vs shallow\n\n**Deep module** = small interface + lots of implementation:\n\n```\n┌─────────────────────┐\n│   Small Interface   │  ← Few methods, simple params\n├─────────────────────┤\n│                     │\n│  Deep Implementation│  ← Complex logic hidden\n│                     │\n└─────────────────────┘\n```\n\n**Shallow module** = large interface + little implementation (avoid):\n\n```\n┌─────────────────────────────────┐\n│       Large Interface           │  ← Many methods, complex params\n├─────────────────────────────────┤\n│  Thin Implementation            │  ← Just passes through\n└─────────────────────────────────┘\n```\n\nWhen designing an interface, ask:\n\n- Can I reduce the number of methods?\n- Can I simplify the parameters?\n- Can I hide more complexity inside?\n\n## Principles\n\n- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.\n- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.\n- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.\n- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.\n\n## Designing for testability\n\nGood interfaces make testing natural:\n\n1. **Accept dependencies, don't create them.**\n\n   ```typescript\n   // Testable\n   function processOrder(order, paymentGateway) {}\n\n   // Hard to test\n   function processOrder(order) {\n     const gateway = new StripeGateway();\n   }\n   ```\n\n2. **Return results, don't produce side effects.**\n\n   ```typescript\n   // Testable\n   function calculateDiscount(cart): Discount {}\n\n   // Hard to test\n   function applyDiscount(cart): void {\n     cart.total -= discount;\n   }\n   ```\n\n3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.\n\n## Relationships\n\n- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).\n- **Depth** is a property of a **Module**, measured against its **Interface**.\n- A **Seam** is where a **Module**'s **Interface** lives.\n- An **Adapter** sits at a **Seam** and satisfies the **Interface**.\n- **Depth** produces **Leverage** for callers and **Locality** for maintainers.\n\n## Rejected framings\n\n- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.\n- **\"Interface\" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.\n- **\"Boundary\"**: overloaded with DDD's bounded context. Say **seam** or **interface**.\n\n## Going deeper\n\n- **Deepening a cluster given its dependencies** — see `.agents/skills/codebase-design/DEEPENING.md` (path from the repo root): dependency categories, seam discipline, and replace-don't-layer testing.\n- **Exploring alternative interfaces** — see `.agents/skills/codebase-design/DESIGN-IT-TWICE.md` (path from the repo root): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement.\n","frontmatter":{"name":"codebase-design","description":"Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary."},"isInternal":false,"tokens":1379,"sizeBytes":6568},{"name":"SKILL.md","path":".agents/skills/commit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/commit/SKILL.md","title":"commit","category":"anthropic-skill","format":"markdown","content":"---\nname: commit\nuser_invocable: true\ndescription: Create a git commit with conventional commit format. MUST use anytime you want to commit changes.\n---\n\n# Git Commit Skill\n\nCreate a focused, single-line commit following conventional commit conventions.\n\n## Instructions\n\n1. **Analyze changes**: Run `git status` and `git diff` to understand what was modified\n2. **Stage only modified files**: Add files individually by name. NEVER use `git add -A` or `git add .`\n3. **Write commit message**: Follow the conventional commit format as a single line\n\n## Conventional Commit Format\n\n```\n<type>: <description>\n```\n\n### Types\n- `feat`: New feature or capability\n- `fix`: Bug fix\n- `refactor`: Code change that neither fixes a bug nor adds a feature\n- `docs`: Documentation only changes\n- `style`: Formatting, missing semicolons, etc (no code change)\n- `test`: Adding or correcting tests\n- `chore`: Maintenance tasks, dependency updates, etc\n- `perf`: Performance improvement\n\n### Rules\n- Message MUST be a single line (no multi-line messages)\n- Description should be lowercase, imperative mood (\"add\" not \"added\")\n- No period at the end\n- Keep under 72 characters total\n\n### Examples\n```\nfeat: add token usage tracking for AI providers\nfix: resolve null pointer in job executor\nrefactor: extract common validation logic\ndocs: update API endpoint documentation\nchore: upgrade sqlx to 0.7\n```\n\n## Execution Steps\n\n1. Run `git status` to see all changes\n2. Run `git diff` to understand the changes in detail\n3. Run `git log --oneline -5` to see recent commit style\n4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`\n5. Create the commit with conventional format:\n   ```bash\n   git commit -m \"<type>: <description>\"\n   ```\n6. Run `git status` to verify the commit succeeded\n","frontmatter":{"name":"commit","user_invocable":true,"description":"Create a git commit with conventional commit format. MUST use anytime you want to commit changes."},"isInternal":false,"tokens":436,"sizeBytes":1787},{"name":"CONTEXT-FORMAT.md","path":".agents/skills/domain-modeling/CONTEXT-FORMAT.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/domain-modeling/CONTEXT-FORMAT.md","title":"Domain-modeling Skill","category":"anthropic-skill","format":"markdown","content":"# Domain-modeling Skill\n\nPath: `.agents/skills/domain-modeling/CONTEXT-FORMAT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/domain-modeling/CONTEXT-FORMAT.md)","tokens":300,"sizeBytes":223},{"name":"SKILL.md","path":".agents/skills/domain-modeling/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/domain-modeling/SKILL.md","title":"domain-modeling","category":"anthropic-skill","format":"markdown","content":"---\nname: domain-modeling\ndescription: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, or when another skill needs to maintain the domain model.\n---\n\n# Domain Modeling\n\nActively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)\n\n## File structure\n\nMost repos have a single context:\n\n```\n/\n├── CONTEXT.md\n└── src/\n```\n\nIf a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:\n\n```\n/\n├── CONTEXT-MAP.md\n└── src/\n    ├── ordering/\n    │   └── CONTEXT.md\n    └── billing/\n        └── CONTEXT.md\n```\n\nCreate files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved.\n\n## During the session\n\n### Challenge against the glossary\n\nWhen the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. \"Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?\"\n\n### Sharpen fuzzy language\n\nWhen the user uses vague or overloaded terms, propose a precise canonical term. \"You're saying 'account' — do you mean the Customer or the User? Those are different things.\"\n\n### Discuss concrete scenarios\n\nWhen domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.\n\n### Cross-reference with code\n\nWhen the user states how something works, check whether the code agrees. If you find a contradiction, surface it: \"Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?\"\n\n### Update CONTEXT.md inline\n\nWhen a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in `.agents/skills/domain-modeling/CONTEXT-FORMAT.md` (path from the repo root).\n\n`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.\n\n","frontmatter":{"name":"domain-modeling","description":"Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, or when another skill needs to maintain the domain model."},"isInternal":false,"tokens":554,"sizeBytes":2551},{"name":"SKILL.md","path":".agents/skills/grill-me/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/grill-me/SKILL.md","title":"grill-me","category":"anthropic-skill","format":"markdown","content":"---\nname: grill-me\ndescription: A relentless interview to sharpen a plan or design.\ndisable-model-invocation: true\n---\n\nRun a `/grilling` session.\n","frontmatter":{"name":"grill-me","description":"A relentless interview to sharpen a plan or design.","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":34,"sizeBytes":147},{"name":"SKILL.md","path":".agents/skills/grilling/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/grilling/SKILL.md","title":"grilling","category":"anthropic-skill","format":"markdown","content":"---\nname: grilling\ndescription: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n---\n\nInterview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.\n\nWork the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.\n\nEach question should be formatted like so:\n\n```\n❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>\n\n➡️ <your recommended answer>\n```\n\nEach round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.\n\nFinding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it — don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report — ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait.\n\nThe session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.\n","frontmatter":{"name":"grilling","description":"Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases."},"isInternal":false,"tokens":395,"sizeBytes":1872},{"name":"HTML-REPORT.md","path":".agents/skills/improve-codebase-architecture/HTML-REPORT.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/improve-codebase-architecture/HTML-REPORT.md","title":"Improve-codebase-architecture Skill","category":"anthropic-skill","format":"markdown","content":"# Improve-codebase-architecture Skill\n\nPath: `.agents/skills/improve-codebase-architecture/HTML-REPORT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/improve-codebase-architecture/HTML-REPORT.md)","tokens":300,"sizeBytes":259},{"name":"SKILL.md","path":".agents/skills/improve-codebase-architecture/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/improve-codebase-architecture/SKILL.md","title":"improve-codebase-architecture","category":"anthropic-skill","format":"markdown","content":"---\nname: improve-codebase-architecture\ndescription: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.\ndisable-model-invocation: true\n---\n\n# Improve Codebase Architecture\n\nSurface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.\n\nThis command is _informed_ by the project's domain model and built on a shared design vocabulary:\n\n- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, \"the interface is the test surface\", \"one adapter = hypothetical seam, two = real\"). Use these terms exactly in every suggestion — don't drift into \"component,\" \"service,\" \"API,\" or \"boundary.\"\n- The domain language in `CONTEXT.md` gives names to good seams.\n\n## Process\n\n### 1. Explore\n\n**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:\n\n- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.\n- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.\n\nRead the project's domain glossary (`CONTEXT.md`) first.\n\nThen spawn a sub-agent to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:\n\n- Where does understanding one concept require bouncing between many small modules?\n- Where are modules **shallow** — interface nearly as complex as the implementation?\n- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?\n- Where do tightly-coupled modules leak across their seams?\n- Which parts of the codebase are untested, or hard to test through their current interface?\n\nApply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A \"yes, concentrates\" is the signal you want.\n\n### 2. Present candidates as an HTML report\n\nWrite a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `<tmpdir>/architecture-review-<timestamp>.html` so each run gets a fresh file. Open it for the user — `xdg-open <path>` on Linux, `open <path>` on macOS, `start <path>` on Windows — and tell them the absolute path.\n\nThe report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.\n\nFor each candidate, render a card with:\n\n- **Files** — which files/modules are involved\n- **Problem** — why the current architecture is causing friction\n- **Solution** — plain English description of what would change\n- **Benefits** — explained in terms of locality and leverage, and how tests would improve\n- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening\n- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge\n\nEnd the report with a **Top recommendation** section: which candidate you'd tackle first and why.\n\n**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines \"Order,\" talk about \"the Order intake module\" — not \"the FooBarHandler,\" and not \"the Order service.\"\n\nSee `.agents/skills/improve-codebase-architecture/HTML-REPORT.md` (path from the repo root) for the full HTML scaffold, diagram patterns, and styling guidance.\n\nDo NOT propose interfaces yet. After the file is written, ask the user: \"Which of these would you like to explore?\"\n\n### 3. Grilling loop\n\nOnce the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.\n\nSide effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:\n\n- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist.\n- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.\n- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.\n","frontmatter":{"name":"improve-codebase-architecture","description":"Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":1168,"sizeBytes":5273},{"name":"SKILL.md","path":".agents/skills/local-review-codex/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/local-review-codex/SKILL.md","title":"local-review-codex","category":"anthropic-skill","format":"markdown","content":"---\nname: local-review-codex\ndescription: Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy, model and reasoning effort as the codex-pr-review GitHub action.\n---\n\n# Local Codex Review (pre-push)\n\nRuns the exact same review Codex performs in CI (`.github/workflows/codex-pr-review.yml`),\nbut locally and scoped to work you have not pushed yet — so you catch what CI would flag\nbefore the PR exists. Use this before `git push` on a non-trivial change.\n\n**Correspondence with CI** — identical:\n- Policy: `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test coverage).\n- Model: `gpt-6.1-sol`.\n- Reasoning effort: `model_reasoning_effort=\"xhigh\"`.\n- Output: markdown starting with `## Codex Review`, findings tagged P0 / P1 / P2 with file:line.\n\n**Differences from CI** — local-only:\n- Scope is the current branch vs `main` at the merge-base, **including uncommitted changes** (CI reviews a pushed PR diff).\n- Sandbox is `read-only` (CI uses `danger-full-access` on an ephemeral runner). Codex reads the diff and files but cannot modify your working tree.\n- Fresh context is inherent: `codex exec` is a separate cold process, so it does not anchor on the current chat session — the same reason `local-review` insists on a subagent.\n\n## Prerequisites\n\n- `codex` CLI **>= 0.159.3** installed and authed via `codex login` or an `OPENAI_API_KEY` in the environment (which takes priority). Older CLIs reject the model with \"requires a newer version of Codex\"; `run.sh` checks the version up front. Upgrade with `npm install --global @openai/codex@0.159.3` (may need `sudo` for a global install). This matches the pin in `.github/workflows/codex-pr-review.yml` — the CLI version is the same on both sides.\n- `git fetch` the base ref if it's stale, so the merge-base is accurate.\n\n## Run\n\n```bash\nbash .agents/skills/local-review-codex/run.sh          # review vs main (default)\nbash .agents/skills/local-review-codex/run.sh <base>   # review vs a different base ref\n```\n\nInvoke with `bash` (or run the executable directly) — the script needs Bash for\n`set -o pipefail`; `sh` is Dash on Debian/Ubuntu and would fail. If `main` isn't a\nlocal branch (e.g. a fresh single-branch checkout), the runner falls back to\n`origin/main` automatically.\n\nThe script computes `BASE_SHA = git merge-base HEAD <base>`, feeds Codex `REVIEW.md` plus a\ndiff context pointing at `git diff <BASE_SHA>` (which folds in uncommitted edits), and prints\nthe review. It writes only temp files — nothing lands in the working tree.\n\n## Relaying the result\n\nPrint the Codex output verbatim. Do not re-summarize or filter it — the value of a cold Codex\npass is surfacing what the current session would rationalize away. Then decide with the user\nwhether to address findings before pushing.\n\nFor a Claude-native review instead, use `local-review` (branch-diff-reviewer subagent). This\nskill is the Codex counterpart; run both for independent perspectives.\n","frontmatter":{"name":"local-review-codex","description":"Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy, model and reasoning effort as the codex-pr-review GitHub action."},"isInternal":false,"tokens":761,"sizeBytes":3033},{"name":"SKILL.md","path":".agents/skills/local-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/local-review/SKILL.md","title":"local-review","category":"anthropic-skill","format":"markdown","content":"---\nname: local-review\ndescription: Code review the current PR (or branch diff against main) for bugs, security, and AGENTS.md compliance. MUST use when asked to review code.\n---\n\n# Local Code Review\n\nRun the same review locally that the GitHub auto-review actions run on PRs (Claude / Codex / Pi). The review policy lives in `REVIEW.md`.\n\n**Why a subagent**: the review MUST run in a fresh context — not inline in the current session. If the user has been iterating on the diff, the main session has absorbed their reasoning and rationalizations, so it anchors and misses things CI catches. A subagent starts cold, like CI does.\n\n## Steps\n\n1. **Determine the PR scope** (cheap, do this in the main session):\n   - If an argument is provided, treat it as a PR number or branch.\n   - Otherwise, detect from the current branch vs `main`.\n   - Confirm the PR/branch exists (`gh pr view <n>` or `git rev-parse <branch>`).\n\n2. **Delegate the review to a fresh-context subagent** with a self-contained prompt. The prompt MUST include:\n   - The PR number or branch name to review.\n   - The instruction to read `REVIEW.md` first for the policy, then `AGENTS.md` files in directories touched by the diff.\n   - The exact output format (see below).\n   - Whether `--comment` was requested (so the subagent emits inline-comment payloads if needed).\n   - Any \"Additional reviewer instructions\" the user provided.\n\n   - **Claude Code**: use the `Agent` tool with `subagent_type: branch-diff-reviewer` (read-only tools, purpose-built for this). If unavailable, fall back to `general-purpose`.\n   - **Codex / Pi**: if the CLI exposes a fresh-session subagent mechanism, use it. Otherwise tell the user to run the skill in a fresh CLI session and stop — running inline in the current session defeats the purpose.\n\n3. **Receive the findings** from the subagent and relay them to the user verbatim. Do not re-summarize, re-judge, or filter — the whole point of fresh context is to surface what the main session would dismiss.\n\n4. **Post comments if `--comment` was requested**: use the `gh` commands below with the subagent's output as the body. The main session does the posting because the subagent is read-only.\n\n## Subagent prompt template\n\n```\nReview <PR #N | branch X> against main per the policy in REVIEW.md.\n\nSteps:\n1. Read REVIEW.md (repo root) for the full policy: severity triage, public-surface\n   checklist, AGENTS.md compliance, test coverage assessment.\n2. Read AGENTS.md (repo root) and any AGENTS.md in directories touched by the diff.\n3. Get the diff: `gh pr diff <N>` (if PR) or `git diff main...<branch>`.\n4. Get context: `gh pr view <N>` (if PR) or `git log main..<branch> --oneline`.\n5. Read changed files only when the diff alone is insufficient to validate a finding.\n6. Self-validate each finding: \"is this definitely a real issue a senior engineer\n   would flag?\" Discard if uncertain.\n7. Output findings in the exact format below. Do not modify any files.\n\n<paste output format from below>\n\n<if --comment requested:>\nAdditionally emit a JSON array of inline comments suitable for the GitHub reviews\nAPI, one per finding that maps to a specific line:\n[{\"path\": \"...\", \"line\": N, \"side\": \"RIGHT\", \"body\": \"[P1] ...\"}, ...]\n```\n\n## Output format\n\n```\n## Code review\n\n<verdict line per REVIEW.md>\n\nFound N issues:\n\n1. [P0|P1|P2] <description>\n   <file_path:line_number>\n\n2. [P0|P1|P2] <description>\n   <file_path:line_number>\n```\n\nEnd with a `Test coverage` section per the shared policy.\n\nIf no issues are found:\n\n```\n## Code review\n\nGood to merge.\n\nNo issues found. Checked for bugs, security, and AGENTS.md compliance.\n```\n\n## Posting comments (`--comment`)\n\nFor a top-level PR comment:\n\n```bash\ngh pr review --comment --body \"<summary from subagent>\"\n```\n\nFor inline comments on specific lines (using the JSON the subagent emitted):\n\n```bash\ngh api repos/{owner}/{repo}/pulls/{pr}/reviews \\\n  -f body=\"<summary>\" -f event=\"COMMENT\" -f comments=\"<json from subagent>\"\n```\n","frontmatter":{"name":"local-review","description":"Code review the current PR (or branch diff against main) for bugs, security, and AGENTS.md compliance. MUST use when asked to review code."},"isInternal":false,"tokens":988,"sizeBytes":3984},{"name":"SKILL.md","path":".agents/skills/native-trigger/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/native-trigger/SKILL.md","title":"native-trigger","category":"anthropic-skill","format":"markdown","content":"---\nname: native-trigger\ndescription: Guidance for adding native trigger services to Windmill. Use when implementing or modifying native trigger integrations across the backend and frontend.\n---\n\n# Skill: Adding Native Trigger Services\n\nThis skill provides comprehensive guidance for adding new native trigger services to Windmill. Native triggers allow external services (like Nextcloud, Google Drive, etc.) to trigger Windmill scripts/flows via webhooks or push notifications.\n\n## Architecture Overview\n\nThe native trigger system consists of:\n\n1. **Database Layer** - PostgreSQL tables and enum types\n2. **Backend Rust Implementation** - Core trait, handlers, and service modules in the `windmill-native-triggers` crate\n3. **Frontend Svelte Components** - Configuration forms and UI components\n\n### Key Files\n\n| Component | Path |\n|-----------|------|\n| Core module with `External` trait | `backend/windmill-native-triggers/src/lib.rs` |\n| Generic CRUD handlers | `backend/windmill-native-triggers/src/handler.rs` |\n| Background sync logic | `backend/windmill-native-triggers/src/sync.rs` |\n| OAuth/workspace integration | `backend/windmill-native-triggers/src/workspace_integrations.rs` |\n| Re-export shim (windmill-api) | `backend/windmill-api/src/native_triggers/mod.rs` |\n| TriggerKind enum | `backend/windmill-common/src/triggers.rs` |\n| JobTriggerKind enum | `backend/windmill-common/src/jobs.rs` |\n| Frontend service registry | `frontend/src/lib/components/triggers/native/utils.ts` |\n| Frontend trigger utilities | `frontend/src/lib/components/triggers/utils.ts` |\n| Trigger badges (icons + counts) | `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte` |\n| Workspace integrations UI | `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte` |\n| OAuth config form component | `frontend/src/lib/components/workspaceSettings/OAuthClientConfig.svelte` |\n| OpenAPI spec | `backend/windmill-api/openapi.yaml` |\n| Reference: Nextcloud module | `backend/windmill-native-triggers/src/nextcloud/` |\n| Reference: Google module | `backend/windmill-native-triggers/src/google/` |\n\n### Crate Structure\n\nThe native trigger code lives in the `windmill-native-triggers` crate (`backend/windmill-native-triggers/`). The `windmill-api` crate re-exports everything via a shim:\n\n```rust\n// backend/windmill-api/src/native_triggers/mod.rs\npub use windmill_native_triggers::*;\n```\n\nAll new service modules go in `backend/windmill-native-triggers/src/`.\n\n---\n\n## Core Concepts\n\n### The `External` Trait\n\nEvery native trigger service implements the `External` trait defined in `lib.rs`:\n\n```rust\n#[async_trait]\npub trait External: Send + Sync + 'static {\n    // Associated types:\n    type ServiceConfig: Debug + DeserializeOwned + Serialize + Send + Sync;\n    type TriggerData: Debug + Serialize + Send + Sync;\n    type OAuthData: DeserializeOwned + Serialize + Clone + Send + Sync;\n    type CreateResponse: DeserializeOwned + Send + Sync;\n\n    // Constants:\n    const SUPPORT_WEBHOOK: bool;\n    const SERVICE_NAME: ServiceName;\n    const DISPLAY_NAME: &'static str;\n    const TOKEN_ENDPOINT: &'static str;\n    const REFRESH_ENDPOINT: &'static str;\n    const AUTH_ENDPOINT: &'static str;\n\n    // Required methods:\n    async fn create(&self, w_id, oauth_data, webhook_token, data, db, tx) -> Result<Self::CreateResponse>;\n    async fn update(&self, w_id, oauth_data, external_id, webhook_token, data, db, tx) -> Result<serde_json::Value>;\n    async fn get(&self, w_id, oauth_data, external_id, db, tx) -> Result<Self::TriggerData>;\n    async fn delete(&self, w_id, oauth_data, external_id, db, tx) -> Result<()>;\n    async fn exists(&self, w_id, oauth_data, external_id, db, tx) -> Result<bool>;\n    async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors);\n    fn external_id_and_metadata_from_response(&self, resp) -> (String, Option<serde_json::Value>);\n\n    // Methods with defaults:\n    async fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned>;\n    fn service_config_from_create_response(&self, data, resp) -> Option<serde_json::Value>;\n    fn additional_routes(&self) -> axum::Router;\n    async fn http_client_request<T, B>(&self, url, method, workspace_id, connection_path, db, headers, body) -> Result<T>;\n}\n```\n\nKey design points:\n- **`update()` returns `serde_json::Value`** - the resolved service_config to store. Each service is responsible for building the final config.\n- **`maintain_triggers()`** - periodic background maintenance. Each service implements its own strategy (Nextcloud: reconcile with external state; Google: renew expiring channels).\n- **No `list_all()` in the trait** - services that need it (Nextcloud) implement it privately; services that don't (Google) use different maintenance strategies.\n- **No `get_external_id_from_trigger_data()` or `extract_service_config_from_trigger_data()`** - removed in favor of the `maintain_triggers` pattern.\n\n### Create Lifecycle: Two Paths\n\nThe `create_native_trigger` handler in `handler.rs` supports two creation flows, controlled by `service_config_from_create_response()`:\n\n**Path A: Short (Google pattern)** - `service_config_from_create_response()` returns `Some(config)`:\n1. `create()` registers on external service\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `service_config_from_create_response()` builds the config directly from input data + response metadata\n4. Stores trigger in DB -- done, no extra round-trip\n\nUse this when the external_id is known before the create call (e.g., Google generates the channel_id as a UUID upfront and includes it in the webhook URL).\n\n**Path B: Long (Nextcloud pattern)** - `service_config_from_create_response()` returns `None` (default):\n1. `create()` registers on external service (webhook URL has no external_id yet)\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `update()` is called to fix the webhook URL with the now-known external_id\n4. `update()` returns the resolved service_config\n5. Stores trigger in DB\n\nUse this when the external_id is assigned by the remote service and the webhook URL needs to be corrected after creation.\n\n### OAuth App and Connections\n\nThe OAuth **app** (client) is configured per workspace and service; **connections** (connected\naccounts) are many per service. Each trigger acts through one connection, named by\n`native_trigger.connection_path`:\n\n| Table | What's Stored |\n|-------|---------------|\n| `workspace_integrations` | `oauth_data` JSON with `base_url`, `client_id`, `client_secret`, `instance_shared` flag. One row per (workspace, service); it must exist for every connection, since token refresh reads the client from it |\n| `variable` | Encrypted `access_token` at the connection path, linked to `account` via its `account` column |\n| `account` | `refresh_token`, with `client` = service name and `is_workspace_integration = true` |\n| `resource` | The connection's resource at the same path, `{ \"token\": \"$var:<path>\" }` |\n\nA user may use a connection when they can read its variable (checked with a user transaction,\nso the variable's row-level security decides): `resolve_usable_connection` /\n`list_usable_connections` in `lib.rs`. The exception is the **workspace connection**: an admin\nmay share one connection per service (`workspace_integrations.resource_path`, set by\n`POST .../workspace_connection`), and every member may then use it without reading its token.\nRenames carry the marker along; disconnecting clears it. Background work (sync, renewal, rename re-registration,\nwebhook handling) uses the stored `connection_path` directly.\n\n`decrypt_oauth_data(db, w_id, service, connection_path)` assembles these into one struct, and\nevery service's `OAuthData` carries `connection_path` so `http_client_request` can refresh and\nwrite back the right connection:\n```rust\npub struct OAuthConfig {\n    pub base_url: String,\n    pub access_token: String,      // decrypted from the connection's variable\n    pub refresh_token: Option<String>, // from its account\n    pub client_id: String,         // from oauth_data or instance settings\n    pub client_secret: String,     // from oauth_data or instance settings\n    pub connection_path: String,\n}\n```\n\nA refreshed token is written back by connection path and account id, never by service: providers\nlike Nextcloud rotate refresh tokens, so writing one onto another account breaks its next refresh.\n\nInstance-level sharing: when the workspace has no OAuth app of its own and the instance admin\nshares one (`share_with_workspaces` in the `oauths` setting; not available for Nextcloud), the\nconnect flow uses it and records `{\"instance_shared\": true}` in `workspace_integrations`.\n\n### URL Resolution\n\nThe `resolve_endpoint()` helper handles both absolute and relative OAuth URLs:\n\n```rust\npub fn resolve_endpoint(base_url: &str, endpoint: &str) -> String {\n    if endpoint.starts_with(\"http://\") || endpoint.starts_with(\"https://\") {\n        endpoint.to_string()  // Google: absolute URLs\n    } else {\n        format!(\"{}{}\", base_url, endpoint)  // Nextcloud: relative paths\n    }\n}\n```\n\n### ServiceName Methods\n\n`ServiceName` is the central registry enum. Each variant must implement these match arms:\n\n| Method | Purpose |\n|--------|---------|\n| `as_str()` | Lowercase identifier (e.g., `\"google\"`) |\n| `as_trigger_kind()` | Maps to `TriggerKind` enum |\n| `as_job_trigger_kind()` | Maps to `JobTriggerKind` enum |\n| `token_endpoint()` | OAuth token endpoint (relative or absolute) |\n| `auth_endpoint()` | OAuth authorization endpoint |\n| `oauth_scopes()` | Space-separated OAuth scopes |\n| `resource_type()` | Resource type for token storage (e.g., `\"gworkspace\"`) |\n| `extra_auth_params()` | Extra OAuth params (e.g., Google needs `access_type=offline`, `prompt=consent`) |\n| `integration_service()` | Maps to the workspace integration service (usually `*self`) |\n| `TryFrom<String>` | Parse from string |\n| `Display` | Delegates to `as_str()` |\n\n---\n\n## Step-by-Step Implementation Guide\n\n### Step 1: Database Migration\n\nCreate a new migration file: `backend/migrations/YYYYMMDDHHMMSS_newservice_trigger.up.sql`\n\n```sql\n-- Add the service to the native_trigger_service enum\nALTER TYPE native_trigger_service ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to TRIGGER_KIND enum (used for trigger tracking)\nALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to job_trigger_kind enum (used for job tracking)\nALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'newservice';\n```\n\nAlso create the corresponding down migration.\n\n### Step 2: Update windmill-common Enums\n\n#### `backend/windmill-common/src/triggers.rs`\n\nAdd variant to `TriggerKind` enum, and update `to_key()` and `fmt()` implementations.\n\n#### `backend/windmill-common/src/jobs.rs`\n\nAdd variant to `JobTriggerKind` enum and update the `Display` implementation.\n\n### Step 3: Backend Service Module\n\nCreate a new directory: `backend/windmill-native-triggers/src/newservice/`\n\n#### `mod.rs` - Type Definitions\n\n```rust\nuse serde::{Deserialize, Serialize};\n\npub mod external;\n// pub mod routes; // Only if you need additional service-specific routes\n\n/// OAuth data deserialized from the three-table pattern.\n/// The actual structure is built by decrypt_oauth_data() from variable + account + workspace_integrations.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct NewServiceOAuthData {\n    pub base_url: String,              // from workspace_integrations.oauth_data\n    pub access_token: String,          // decrypted from variable table\n    pub refresh_token: Option<String>, // from account table\n    // Note: client_id and client_secret are in OAuthConfig, not here\n    // unless the service needs them at runtime for API calls\n}\n\n/// Configuration provided by user when creating/updating a trigger.\n/// Stored as JSON in native_trigger.service_config.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceConfig {\n    // Service-specific configuration fields\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n}\n\n/// Data retrieved from the external service about a trigger.\n/// Returned by the get() method and shown in the UI.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceTriggerData {\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n    // Fields that shouldn't affect service_config comparison should use #[serde(skip_serializing)]\n}\n\n/// Response from external service when creating a trigger/webhook.\n#[derive(Debug, Deserialize)]\npub struct CreateTriggerResponse {\n    pub id: String,\n}\n\n/// Handler struct (stateless, used for routing)\n#[derive(Copy, Clone)]\npub struct NewService;\n```\n\n#### `external.rs` - External Trait Implementation\n\n```rust\nuse async_trait::async_trait;\nuse reqwest::Method;\nuse sqlx::PgConnection;\nuse std::collections::HashMap;\nuse windmill_common::{\n    error::{Error, Result},\n    BASE_URL, DB,\n};\n\nuse crate::{\n    generate_webhook_service_url, External, NativeTrigger, NativeTriggerData, ServiceName,\n    sync::{SyncError, TriggerSyncInfo},\n};\nuse super::{NewService, NewServiceConfig, NewServiceOAuthData, NewServiceTriggerData, CreateTriggerResponse};\n\n#[async_trait]\nimpl External for NewService {\n    type ServiceConfig = NewServiceConfig;\n    type TriggerData = NewServiceTriggerData;\n    type OAuthData = NewServiceOAuthData;\n    type CreateResponse = CreateTriggerResponse;\n\n    const SERVICE_NAME: ServiceName = ServiceName::NewService;\n    const DISPLAY_NAME: &'static str = \"New Service\";\n    const SUPPORT_WEBHOOK: bool = true;\n    const TOKEN_ENDPOINT: &'static str = \"/oauth/token\";\n    const REFRESH_ENDPOINT: &'static str = \"/oauth/token\";\n    const AUTH_ENDPOINT: &'static str = \"/oauth/authorize\";\n\n    async fn create(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::CreateResponse> {\n        let base_url = &*BASE_URL.read().await;\n\n        // external_id is None during create (we get it from the response)\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            None, Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/create\", oauth_data.base_url);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let response: CreateTriggerResponse = self\n            .http_client_request(&url, Method::POST, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        Ok(response)\n    }\n\n    /// Update returns the resolved service_config as JSON.\n    /// For services using the update+get pattern, call self.get() and serialize.\n    async fn update(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<serde_json::Value> {\n        let base_url = &*BASE_URL.read().await;\n\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            Some(external_id), Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let _: serde_json::Value = self\n            .http_client_request(&url, Method::PUT, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        // Fetch back the updated state to get the resolved config\n        let trigger_data = self.get(w_id, oauth_data, external_id, db, tx).await?;\n        serde_json::to_value(&trigger_data)\n            .map_err(|e| Error::InternalErr(format!(\"Failed to serialize trigger data: {}\", e)))\n    }\n\n    async fn get(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::TriggerData> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        self.http_client_request::<_, ()>(&url, Method::GET, w_id, &oauth_data.connection_path, db, None, None).await\n    }\n\n    async fn delete(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<()> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let _: serde_json::Value = self\n            .http_client_request::<_, ()>(&url, Method::DELETE, w_id, &oauth_data.connection_path, db, None, None)\n            .await\n            .or_else(|e| match &e {\n                Error::InternalErr(msg) if msg.contains(\"404\") => Ok(serde_json::Value::Null),\n                _ => Err(e),\n            })?;\n        Ok(())\n    }\n\n    async fn exists(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<bool> {\n        match self.get(w_id, oauth_data, external_id, db, tx).await {\n            Ok(_) => Ok(true),\n            Err(Error::NotFound(_)) => Ok(false),\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Background maintenance. Choose the right pattern for your service:\n    /// - For services with queryable external state: use reconcile_with_external_state()\n    /// - For channel-based services with expiration: implement renewal logic\n    async fn maintain_triggers(\n        &self,\n        db: &DB,\n        workspace_id: &str,\n        triggers: &[NativeTrigger],\n        oauth_data: &Self::OAuthData,\n        synced: &mut Vec<TriggerSyncInfo>,\n        errors: &mut Vec<SyncError>,\n    ) {\n        // Option A: Reconcile with external state (Nextcloud pattern)\n        // Fetch all triggers from external service and compare with DB\n        let external_triggers = match self.list_all(workspace_id, oauth_data, db).await {\n            Ok(triggers) => triggers,\n            Err(e) => {\n                errors.push(SyncError {\n                    resource_path: format!(\"workspace:{}\", workspace_id),\n                    error_message: format!(\"Failed to list triggers: {}\", e),\n                    error_type: \"api_error\".to_string(),\n                });\n                return;\n            }\n        };\n\n        // Convert to (external_id, config_json) pairs\n        let external_pairs: Vec<(String, serde_json::Value)> = external_triggers\n            .into_iter()\n            .map(|t| (t.id.clone(), serde_json::to_value(&t).unwrap_or_default()))\n            .collect();\n\n        crate::sync::reconcile_with_external_state(\n            db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n        ).await;\n    }\n\n    fn external_id_and_metadata_from_response(\n        &self,\n        resp: &Self::CreateResponse,\n    ) -> (String, Option<serde_json::Value>) {\n        (resp.id.clone(), None)\n    }\n\n    // service_config_from_create_response: NOT overridden (returns None).\n    // This means the handler uses the update+get pattern after create.\n    // Override and return Some(...) to skip the update+get cycle (Google pattern).\n}\n\nimpl NewService {\n    /// Private helper to list all triggers from the external service.\n    async fn list_all(\n        &self,\n        w_id: &str,\n        oauth_data: &<Self as External>::OAuthData,\n        db: &DB,\n    ) -> Result<Vec<<Self as External>::TriggerData>> {\n        // Implementation depends on the external service's API\n        todo!()\n    }\n}\n```\n\n### Step 4: Update lib.rs Registry\n\nIn `backend/windmill-native-triggers/src/lib.rs`:\n\n```rust\n// Service modules - add new services here:\n#[cfg(feature = \"native_trigger\")]\npub mod newservice;  // <-- Add this\n\n// ServiceName enum - add variant:\npub enum ServiceName {\n    Nextcloud,\n    Google,\n    NewService,  // <-- Add this\n}\n\n// Then add match arms in ALL ServiceName methods:\n// as_str(), as_trigger_kind(), as_job_trigger_kind(), token_endpoint(),\n// auth_endpoint(), oauth_scopes(), resource_type(), extra_auth_params(),\n// integration_service(), TryFrom<String>, Display\n```\n\n### Step 5: Update handler.rs Routes\n\nIn `backend/windmill-native-triggers/src/handler.rs`:\n\n```rust\npub fn generate_native_trigger_routers() -> Router {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n        return router\n            .nest(\"/nextcloud\", service_routes(NextCloud))\n            .nest(\"/google\", service_routes(Google))\n            .nest(\"/newservice\", service_routes(NewService));  // <-- Add this\n    }\n    // ...\n}\n```\n\n### Step 6: Update sync.rs\n\nIn `backend/windmill-native-triggers/src/sync.rs`:\n\n```rust\npub async fn sync_all_triggers(db: &DB) -> Result<BackgroundSyncResult> {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n\n        // ... existing service syncs ...\n\n        // New service sync\n        let (service_name, result) = sync_service_triggers(db, NewService).await;\n        total_synced += result.synced_triggers.len();\n        total_errors += result.errors.len();\n        service_results.insert(service_name, result);\n    }\n    // ...\n}\n```\n\n### Step 7: Frontend Service Registry\n\nIn `frontend/src/lib/components/triggers/native/utils.ts`:\n\nAdd to `NATIVE_TRIGGER_SERVICES`, `getTriggerIconName()`, and `getServiceIcon()`.\n\n### Step 8: Frontend Trigger Form Component\n\nCreate: `frontend/src/lib/components/triggers/native/services/newservice/NewServiceTriggerForm.svelte`\n\n### Step 9: Frontend Icon Component\n\nCreate: `frontend/src/lib/components/icons/NewServiceIcon.svelte`\n\n### Step 10: Update NativeTriggerEditor\n\nCheck `frontend/src/lib/components/triggers/native/NativeTriggerEditor.svelte` to ensure it dynamically loads form components based on service name.\n\n### Step 11: Workspace Integration UI\n\nAdd your service to the `supportedServices` map in `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte`:\n\n```typescript\nconst supportedServices: Record<string, ServiceConfig> = {\n    // ... existing services ...\n    newservice: {\n        name: 'newservice',\n        displayName: 'New Service',\n        description: 'Connect to New Service for triggers',\n        icon: NewServiceIcon,\n        docsUrl: 'https://www.windmill.dev/docs/integrations/newservice',\n        requiresBaseUrl: false,  // false for cloud services, true for self-hosted\n        setupInstructions: [\n            'Step 1: Create an OAuth app on the service',\n            'Step 2: Configure the redirect URI shown below',\n            'Step 3: Enter the client credentials below'\n        ]\n    }\n}\n```\n\n### Step 12: Update `frontend/src/lib/components/triggers/utils.ts`\n\nUpdate ALL of these maps/functions:\n1. `triggerIconMap` - import and add icon\n2. `triggerDisplayNamesMap` - add display name\n3. `triggerTypeOrder` in `sortTriggers()` - add type\n4. `getLightConfig()` - add case for your service\n5. `getTriggerLabel()` - add case for your service\n6. `jobTriggerKinds` - add to array\n7. `countPropertyMap` - add count property\n8. `triggerSaveFunctions` - add save function\n\n### Step 13: Update TriggersBadge Component\n\nIn `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte`:\n\n1. Import the icon\n2. Add to `baseConfig` with `countKey` (the dynamic `availableNativeServices` loop does NOT set `countKey`)\n3. Add to the `allTypes` array\n\n### Step 14: Update TriggersWrapper.svelte\n\nIn `frontend/src/lib/components/triggers/TriggersWrapper.svelte`:\n\nAdd a `{:else if selectedTrigger.type === 'yourservice'}` case that renders `<NativeTriggersPanel service=\"yourservice\" ...>` with the same props pattern as the existing native trigger cases (e.g., `nextcloud`).\n\n### Step 15: Update AddTriggersButton.svelte\n\nIn `frontend/src/lib/components/triggers/AddTriggersButton.svelte`:\n\n1. Add `yourserviceAvailable` state variable\n2. Add `setYourserviceState()` async function using `isServiceAvailable('yourservice', $workspaceStore!)`\n3. Call it at module level\n4. Add a dropdown entry to `addTriggerItems` with `hidden: !yourserviceAvailable`\n\n### Step 16: Update TriggersEditor.svelte Delete Handling\n\nIn `frontend/src/lib/components/triggers/TriggersEditor.svelte`:\n\nAdd your service to the `nativeTriggerServices` map in `deleteDeployedTrigger()`. Native triggers use `NativeTriggerService.deleteNativeTrigger({ workspace, serviceName, externalId })` instead of the standard `path`-based delete.\n\n### Step 17: Update `getUsedTriggers` for Sidebar Visibility\n\nThe sidebar (`frontend/src/lib/components/sidebar/SidebarContent.svelte`) shows native-trigger links only if `$usedTriggerKinds` includes the service — without this, your trigger page will never appear in the nav bar even when triggers exist.\n\n1. **Backend** — add `{service}_used: bool` to the `UsedTriggers` struct and SELECT in `backend/windmill-api-workspaces/src/workspaces.rs::get_used_triggers()`:\n   ```rust\n   EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = '{service}'::native_trigger_service) AS \"{service}_used!\"\n   ```\n2. **OpenAPI** — add `{service}_used: boolean` to the response schema for `GET /w/{workspace}/workspaces/used_triggers` (under both `properties` and `required`).\n3. **Layout** — in `frontend/src/routes/(root)/(logged)/+layout.svelte::loadUsedTriggerKinds()`, destructure `{service}_used` and push `'{service}'` to `usedKinds`.\n\n### Step 18: Update OpenAPI Spec and Regenerate Types\n\nAdd to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:\n\n```bash\ncd frontend && npm run generate-backend-client\n```\n\n---\n\n## Special Patterns\n\n### Unified Service with `trigger_type` (Google Pattern)\n\nWhen a single service handles multiple trigger types (e.g., Google Drive + Calendar share OAuth and API patterns), use a single `ServiceName` variant with a discriminator field:\n\n```rust\npub enum GoogleTriggerType { Drive, Calendar }\n\npub struct GoogleServiceConfig {\n    pub trigger_type: GoogleTriggerType,\n    // Drive-specific fields (only used when trigger_type = Drive)\n    pub resource_id: Option<String>,\n    pub resource_name: Option<String>,\n    // Calendar-specific fields (only used when trigger_type = Calendar)\n    pub calendar_id: Option<String>,\n    pub calendar_name: Option<String>,\n    // Metadata set after creation\n    pub google_resource_id: Option<String>,\n    pub expiration: Option<String>,\n}\n```\n\nBranch in trait methods based on `trigger_type`. Frontend uses a `ToggleButtonGroup` to switch between types. This keeps the codebase simpler (one service, one OAuth flow, one set of routes).\n\nSee `backend/windmill-native-triggers/src/google/` for the reference implementation.\n\n### Skipping update+get After Create (Google Pattern)\n\nOverride `service_config_from_create_response()` to return `Some(config)` when the external_id is known before the create call:\n\n```rust\nfn service_config_from_create_response(\n    &self,\n    data: &NativeTriggerData<Self::ServiceConfig>,\n    resp: &Self::CreateResponse,\n) -> Option<serde_json::Value> {\n    // Clone input config, add metadata from response\n    let mut config = data.service_config.clone();\n    config.google_resource_id = Some(resp.resource_id.clone());\n    config.expiration = Some(resp.expiration.clone());\n    Some(serde_json::to_value(&config).unwrap())\n}\n```\n\n### Services with Absolute OAuth Endpoints (Google)\n\nUnlike self-hosted services where OAuth endpoints are relative paths appended to `base_url`, services like Google have absolute URLs:\n\n```rust\n// Nextcloud: relative paths\nServiceName::Nextcloud => \"/apps/oauth2/api/v1/token\",\n// Google: absolute URLs\nServiceName::Google => \"https://oauth2.googleapis.com/token\",\n```\n\nThe `resolve_endpoint()` function handles both. For services with absolute endpoints:\n- `base_url` can be empty\n- `requiresBaseUrl: false` in the frontend workspace integration config\n- Add `extra_auth_params()` if needed (Google requires `access_type=offline` and `prompt=consent`)\n\n### Channel-Based Push Notifications with Renewal (Google Pattern)\n\nFor services using expiring watch channels instead of persistent webhooks:\n\n1. Store expiration in `service_config` (as part of `ServiceConfig`)\n2. In `maintain_triggers()`, implement renewal logic instead of using `reconcile_with_external_state()`:\n   ```rust\n   async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors) {\n       for trigger in triggers {\n           if should_renew_channel(trigger) {\n               self.renew_channel(db, trigger, oauth_data).await;\n           }\n       }\n   }\n   ```\n3. Renewal: best-effort stop old channel, create new one with same external_id, update service_config with new expiration\n4. Google example: Drive channels expire in 24h (renew when <1h left), Calendar channels expire in 7 days (renew when <1 day left)\n\n### reconcile_with_external_state (Nextcloud Pattern)\n\nThe reusable function in `sync.rs` compares external triggers with DB state:\n- Triggers missing externally: sets error \"Trigger no longer exists on external service\"\n- Triggers present externally: clears errors, updates service_config if it differs\n\nUsage in `maintain_triggers()`:\n```rust\nlet external_pairs: Vec<(String, serde_json::Value)> = /* fetch from external */;\ncrate::sync::reconcile_with_external_state(\n    db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n).await;\n```\n\n### Webhook Payload Processing\n\nOverride `prepare_webhook()` to parse service-specific payloads into script/flow args:\n\n```rust\nasync fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned> {\n    let mut args = HashMap::new();\n    args.insert(\"event_type\".to_string(), Box::new(headers.get(\"x-event-type\").cloned()) as _);\n    args.insert(\"payload\".to_string(), Box::new(serde_json::from_str::<serde_json::Value>(&body)?) as _);\n    Ok(PushArgsOwned { extra: None, args })\n}\n```\n\nThen register in `prepare_native_trigger_args()` in `lib.rs`:\n```rust\npub async fn prepare_native_trigger_args(service_name, db, w_id, headers, body) -> Result<Option<PushArgsOwned>> {\n    match service_name {\n        ServiceName::Google => { /* ... */ Ok(Some(args)) }\n        ServiceName::NewService => { /* ... */ Ok(Some(args)) }\n        ServiceName::Nextcloud => Ok(None), // Uses default body parsing\n    }\n}\n```\n\n### Instance-Level OAuth Credentials\n\nWhen `workspace_integrations.oauth_data.instance_shared == true`, `decrypt_oauth_data()` reads `client_id` and `client_secret` from instance-level global settings instead of workspace-level. This allows admins to share OAuth app credentials across workspaces.\n\n`generate_connect_url` picks the client (`resolve_oauth_client`): the workspace's own app when configured, the instance's otherwise. A GitHub OAuth app only redirects below its single registered callback, so connections made with the instance GitHub app land on `/oauth/callback/github/native_trigger` instead of the workspace settings page.\n\n---\n\n## Testing Checklist\n\n- [ ] Database migration runs successfully\n- [ ] `cargo check -p windmill-native-triggers --features native_trigger` passes\n- [ ] `npx svelte-check --threshold error` passes (in frontend/)\n- [ ] Service appears in workspace integrations list\n- [ ] OAuth flow completes successfully\n- [ ] Can create a new trigger\n- [ ] Can view trigger details\n- [ ] Can update trigger configuration\n- [ ] Can delete trigger\n- [ ] Webhook receives and processes payloads\n- [ ] Background sync works correctly (reconciliation or channel renewal)\n- [ ] Error handling works (expired tokens, service unavailable)\n\n---\n\n## Reference Implementations\n\n### Nextcloud (Self-Hosted, Update+Get Pattern)\n\n| File | Purpose |\n|------|---------|\n| `nextcloud/mod.rs` | Types: NextCloudOAuthData, NextcloudServiceConfig, NextCloudTriggerData |\n| `nextcloud/external.rs` | External trait: uses update+get pattern, reconcile_with_external_state for sync |\n| `nextcloud/routes.rs` | Additional route: `GET /events` |\n\nKey patterns: relative OAuth endpoints, base_url required, list_all + reconcile for sync, update returns JSON from get().\n\n### Google (Cloud, Unified Service, Short Create)\n\n| File | Purpose |\n|------|---------|\n| `google/mod.rs` | Types: GoogleServiceConfig with trigger_type discriminator, GoogleTriggerType enum |\n| `google/external.rs` | External trait: overrides service_config_from_create_response, channel renewal for sync |\n| `google/routes.rs` | Additional routes: `GET /calendars`, `GET /drive/files`, `GET /drive/shared_drives` |\n\nKey patterns: absolute OAuth endpoints, empty base_url, trigger_type for Drive/Calendar, expiring watch channels with renewal, service_config_from_create_response skips update+get, get() reconstructs data from stored service_config (no external \"get channel\" API).\n","frontmatter":{"name":"native-trigger","description":"Guidance for adding native trigger services to Windmill. Use when implementing or modifying native trigger integrations across the backend and frontend."},"isInternal":false,"tokens":7673,"sizeBytes":33348},{"name":"SKILL.md","path":".agents/skills/pr/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/pr/SKILL.md","title":"pr","category":"anthropic-skill","format":"markdown","content":"# pr\n\nPath: `.agents/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/pr/SKILL.md)","tokens":300,"sizeBytes":160},{"name":"SKILL.md","path":".agents/skills/refine/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/refine/SKILL.md","title":"refine","category":"anthropic-skill","format":"markdown","content":"# refine\n\nPath: `.agents/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/refine/SKILL.md)","tokens":300,"sizeBytes":172},{"name":"SKILL.md","path":".agents/skills/rust-backend/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/rust-backend/SKILL.md","title":"rust-backend","category":"anthropic-skill","format":"markdown","content":"# rust-backend\n\nPath: `.agents/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/rust-backend/SKILL.md)","tokens":300,"sizeBytes":190},{"name":"SKILL.md","path":".agents/skills/svelte-frontend/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/svelte-frontend/SKILL.md","title":"svelte-frontend","category":"anthropic-skill","format":"markdown","content":"# svelte-frontend\n\nPath: `.agents/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/svelte-frontend/SKILL.md)","tokens":300,"sizeBytes":199},{"name":"SKILL.md","path":".agents/skills/update-sqlx/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/update-sqlx/SKILL.md","title":"update-sqlx","category":"anthropic-skill","format":"markdown","content":"# update-sqlx\n\nPath: `.agents/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/update-sqlx/SKILL.md)","tokens":300,"sizeBytes":187},{"name":"UPSTREAM.md","path":".agents/skills/UPSTREAM.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/UPSTREAM.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# Skills Skill\n\nPath: `.agents/skills/UPSTREAM.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/UPSTREAM.md)","tokens":300,"sizeBytes":170},{"name":"SKILL.md","path":".claude/skills/adding-a-trigger/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/adding-a-trigger/SKILL.md","title":"adding-a-trigger","category":"anthropic-skill","format":"markdown","content":"# adding-a-trigger\n\nPath: `.claude/skills/adding-a-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/adding-a-trigger/SKILL.md)","tokens":300,"sizeBytes":202},{"name":"SKILL.md","path":".claude/skills/ai-chat/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-chat/SKILL.md","title":"ai-chat","category":"anthropic-skill","format":"markdown","content":"# ai-chat\n\nPath: `.claude/skills/ai-chat/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-chat/SKILL.md)","tokens":300,"sizeBytes":175},{"name":"SKILL.md","path":".claude/skills/ai-evals/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-evals/SKILL.md","title":"ai-evals","category":"anthropic-skill","format":"markdown","content":"# ai-evals\n\nPath: `.claude/skills/ai-evals/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-evals/SKILL.md)","tokens":300,"sizeBytes":178},{"name":"SKILL.md","path":".claude/skills/codebase-design/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/codebase-design/SKILL.md","title":"codebase-design","category":"anthropic-skill","format":"markdown","content":"# codebase-design\n\nPath: `.claude/skills/codebase-design/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/codebase-design/SKILL.md)","tokens":300,"sizeBytes":199},{"name":"SKILL.md","path":".claude/skills/commit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/commit/SKILL.md","title":"commit","category":"anthropic-skill","format":"markdown","content":"# commit\n\nPath: `.claude/skills/commit/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/commit/SKILL.md)","tokens":300,"sizeBytes":172},{"name":"SKILL.md","path":".claude/skills/domain-modeling/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/domain-modeling/SKILL.md","title":"domain-modeling","category":"anthropic-skill","format":"markdown","content":"# domain-modeling\n\nPath: `.claude/skills/domain-modeling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/domain-modeling/SKILL.md)","tokens":300,"sizeBytes":199},{"name":"SKILL.md","path":".claude/skills/grill-me/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grill-me/SKILL.md","title":"grill-me","category":"anthropic-skill","format":"markdown","content":"# grill-me\n\nPath: `.claude/skills/grill-me/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grill-me/SKILL.md)","tokens":300,"sizeBytes":178},{"name":"SKILL.md","path":".claude/skills/grilling/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grilling/SKILL.md","title":"grilling","category":"anthropic-skill","format":"markdown","content":"# grilling\n\nPath: `.claude/skills/grilling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/grilling/SKILL.md)","tokens":300,"sizeBytes":178},{"name":"SKILL.md","path":".claude/skills/improve-codebase-architecture/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/improve-codebase-architecture/SKILL.md","title":"improve-codebase-architecture","category":"anthropic-skill","format":"markdown","content":"# improve-codebase-architecture\n\nPath: `.claude/skills/improve-codebase-architecture/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/improve-codebase-architecture/SKILL.md)","tokens":300,"sizeBytes":241},{"name":"SKILL.md","path":".claude/skills/local-review-codex/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review-codex/SKILL.md","title":"local-review-codex","category":"anthropic-skill","format":"markdown","content":"# local-review-codex\n\nPath: `.claude/skills/local-review-codex/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review-codex/SKILL.md)","tokens":300,"sizeBytes":208},{"name":"SKILL.md","path":".claude/skills/local-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review/SKILL.md","title":"local-review","category":"anthropic-skill","format":"markdown","content":"# local-review\n\nPath: `.claude/skills/local-review/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/local-review/SKILL.md)","tokens":300,"sizeBytes":190},{"name":"SKILL.md","path":".claude/skills/native-trigger/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/native-trigger/SKILL.md","title":"native-trigger","category":"anthropic-skill","format":"markdown","content":"# native-trigger\n\nPath: `.claude/skills/native-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/native-trigger/SKILL.md)","tokens":300,"sizeBytes":196},{"name":"SKILL.md","path":".claude/skills/pr/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/pr/SKILL.md","title":"pr","category":"anthropic-skill","format":"markdown","content":"# pr\n\nPath: `.claude/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/pr/SKILL.md)","tokens":300,"sizeBytes":160},{"name":"SKILL.md","path":".claude/skills/refine/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/refine/SKILL.md","title":"refine","category":"anthropic-skill","format":"markdown","content":"# refine\n\nPath: `.claude/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/refine/SKILL.md)","tokens":300,"sizeBytes":172},{"name":"SKILL.md","path":".claude/skills/rust-backend/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/rust-backend/SKILL.md","title":"rust-backend","category":"anthropic-skill","format":"markdown","content":"# rust-backend\n\nPath: `.claude/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/rust-backend/SKILL.md)","tokens":300,"sizeBytes":190},{"name":"SKILL.md","path":".claude/skills/svelte-frontend/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/svelte-frontend/SKILL.md","title":"svelte-frontend","category":"anthropic-skill","format":"markdown","content":"# svelte-frontend\n\nPath: `.claude/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/svelte-frontend/SKILL.md)","tokens":300,"sizeBytes":199},{"name":"SKILL.md","path":".claude/skills/update-sqlx/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/update-sqlx/SKILL.md","title":"update-sqlx","category":"anthropic-skill","format":"markdown","content":"# update-sqlx\n\nPath: `.claude/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/update-sqlx/SKILL.md)","tokens":300,"sizeBytes":187},{"name":"flow.yaml","path":"ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml","title":"Hello__flow Skill","category":"anthropic-skill","format":"yaml","content":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml)","tokens":300,"sizeBytes":297},{"name":"flow.yaml","path":"ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml","title":"Hello__flow Skill","category":"anthropic-skill","format":"yaml","content":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/bun-hello-flow/f/evals/hello__flow/flow.yaml)","tokens":300,"sizeBytes":273},{"name":"flow.yaml","path":"ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml","title":"Reuse_greeting__flow Skill","category":"anthropic-skill","format":"yaml","content":"# Reuse_greeting__flow Skill\n\nPath: `ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/expected/flow-reuse-existing-script/f/evals/reuse_greeting__flow/flow.yaml)","tokens":300,"sizeBytes":324},{"name":"flow.yaml","path":"ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml","title":"Hello__flow Skill","category":"anthropic-skill","format":"yaml","content":"# Hello__flow Skill\n\nPath: `ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/fixtures/cli/initial/bun-hello-flow-punctuation/f/evals/hello__flow/flow.yaml)","tokens":300,"sizeBytes":295},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/cli-commands/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/cli-commands/SKILL.md","title":"cli-commands","category":"anthropic-skill","format":"markdown","content":"# cli-commands\n\nPath: `system_prompts/auto-generated/skills/cli-commands/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/cli-commands/SKILL.md)","tokens":300,"sizeBytes":234},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/datatables/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/datatables/SKILL.md","title":"datatables","category":"anthropic-skill","format":"markdown","content":"# datatables\n\nPath: `system_prompts/auto-generated/skills/datatables/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/datatables/SKILL.md)","tokens":300,"sizeBytes":228},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/preview/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/preview/SKILL.md","title":"preview","category":"anthropic-skill","format":"markdown","content":"# preview\n\nPath: `system_prompts/auto-generated/skills/preview/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/preview/SKILL.md)","tokens":300,"sizeBytes":219},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/raw-app/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/raw-app/SKILL.md","title":"raw-app","category":"anthropic-skill","format":"markdown","content":"# raw-app\n\nPath: `system_prompts/auto-generated/skills/raw-app/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/raw-app/SKILL.md)","tokens":300,"sizeBytes":219},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/resources/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/resources/SKILL.md","title":"resources","category":"anthropic-skill","format":"markdown","content":"# resources\n\nPath: `system_prompts/auto-generated/skills/resources/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/resources/SKILL.md)","tokens":300,"sizeBytes":225},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/schedules/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/schedules/SKILL.md","title":"schedules","category":"anthropic-skill","format":"markdown","content":"# schedules\n\nPath: `system_prompts/auto-generated/skills/schedules/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/schedules/SKILL.md)","tokens":300,"sizeBytes":225},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/triggers/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/triggers/SKILL.md","title":"triggers","category":"anthropic-skill","format":"markdown","content":"# triggers\n\nPath: `system_prompts/auto-generated/skills/triggers/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/triggers/SKILL.md)","tokens":300,"sizeBytes":222},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/workspace-settings/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/workspace-settings/SKILL.md","title":"workspace-settings","category":"anthropic-skill","format":"markdown","content":"# workspace-settings\n\nPath: `system_prompts/auto-generated/skills/workspace-settings/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/workspace-settings/SKILL.md)","tokens":300,"sizeBytes":252},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-flow/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-flow/SKILL.md","title":"write-flow","category":"anthropic-skill","format":"markdown","content":"# write-flow\n\nPath: `system_prompts/auto-generated/skills/write-flow/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-flow/SKILL.md)","tokens":300,"sizeBytes":228},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-pipeline/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-pipeline/SKILL.md","title":"write-pipeline","category":"anthropic-skill","format":"markdown","content":"# write-pipeline\n\nPath: `system_prompts/auto-generated/skills/write-pipeline/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-pipeline/SKILL.md)","tokens":300,"sizeBytes":240},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-ansible/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-ansible/SKILL.md","title":"write-script-ansible","category":"anthropic-skill","format":"markdown","content":"# write-script-ansible\n\nPath: `system_prompts/auto-generated/skills/write-script-ansible/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-ansible/SKILL.md)","tokens":300,"sizeBytes":258},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-bash/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bash/SKILL.md","title":"write-script-bash","category":"anthropic-skill","format":"markdown","content":"# write-script-bash\n\nPath: `system_prompts/auto-generated/skills/write-script-bash/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bash/SKILL.md)","tokens":300,"sizeBytes":249},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md","title":"write-script-bigquery","category":"anthropic-skill","format":"markdown","content":"# write-script-bigquery\n\nPath: `system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bigquery/SKILL.md)","tokens":300,"sizeBytes":261},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-bun/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bun/SKILL.md","title":"write-script-bun","category":"anthropic-skill","format":"markdown","content":"# write-script-bun\n\nPath: `system_prompts/auto-generated/skills/write-script-bun/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bun/SKILL.md)","tokens":300,"sizeBytes":246},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md","title":"write-script-bunnative","category":"anthropic-skill","format":"markdown","content":"# write-script-bunnative\n\nPath: `system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md)","tokens":300,"sizeBytes":264},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-csharp/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md","title":"write-script-csharp","category":"anthropic-skill","format":"markdown","content":"# write-script-csharp\n\nPath: `system_prompts/auto-generated/skills/write-script-csharp/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-csharp/SKILL.md)","tokens":300,"sizeBytes":255},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-deno/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-deno/SKILL.md","title":"write-script-deno","category":"anthropic-skill","format":"markdown","content":"# write-script-deno\n\nPath: `system_prompts/auto-generated/skills/write-script-deno/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-deno/SKILL.md)","tokens":300,"sizeBytes":249},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md","title":"write-script-duckdb","category":"anthropic-skill","format":"markdown","content":"# write-script-duckdb\n\nPath: `system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-duckdb/SKILL.md)","tokens":300,"sizeBytes":255},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-go/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-go/SKILL.md","title":"write-script-go","category":"anthropic-skill","format":"markdown","content":"# write-script-go\n\nPath: `system_prompts/auto-generated/skills/write-script-go/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-go/SKILL.md)","tokens":300,"sizeBytes":243},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-graphql/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md","title":"write-script-graphql","category":"anthropic-skill","format":"markdown","content":"# write-script-graphql\n\nPath: `system_prompts/auto-generated/skills/write-script-graphql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-graphql/SKILL.md)","tokens":300,"sizeBytes":258},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-java/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-java/SKILL.md","title":"write-script-java","category":"anthropic-skill","format":"markdown","content":"# write-script-java\n\nPath: `system_prompts/auto-generated/skills/write-script-java/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-java/SKILL.md)","tokens":300,"sizeBytes":249},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-mssql/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md","title":"write-script-mssql","category":"anthropic-skill","format":"markdown","content":"# write-script-mssql\n\nPath: `system_prompts/auto-generated/skills/write-script-mssql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mssql/SKILL.md)","tokens":300,"sizeBytes":252},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-mysql/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md","title":"write-script-mysql","category":"anthropic-skill","format":"markdown","content":"# write-script-mysql\n\nPath: `system_prompts/auto-generated/skills/write-script-mysql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-mysql/SKILL.md)","tokens":300,"sizeBytes":252},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-php/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-php/SKILL.md","title":"write-script-php","category":"anthropic-skill","format":"markdown","content":"# write-script-php\n\nPath: `system_prompts/auto-generated/skills/write-script-php/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-php/SKILL.md)","tokens":300,"sizeBytes":246},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md","title":"write-script-postgresql","category":"anthropic-skill","format":"markdown","content":"# write-script-postgresql\n\nPath: `system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-postgresql/SKILL.md)","tokens":300,"sizeBytes":267},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-powershell/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md","title":"write-script-powershell","category":"anthropic-skill","format":"markdown","content":"# write-script-powershell\n\nPath: `system_prompts/auto-generated/skills/write-script-powershell/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-powershell/SKILL.md)","tokens":300,"sizeBytes":267},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-python3/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-python3/SKILL.md","title":"write-script-python3","category":"anthropic-skill","format":"markdown","content":"# write-script-python3\n\nPath: `system_prompts/auto-generated/skills/write-script-python3/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-python3/SKILL.md)","tokens":300,"sizeBytes":258},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-rlang/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md","title":"write-script-rlang","category":"anthropic-skill","format":"markdown","content":"# write-script-rlang\n\nPath: `system_prompts/auto-generated/skills/write-script-rlang/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rlang/SKILL.md)","tokens":300,"sizeBytes":252},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-rust/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rust/SKILL.md","title":"write-script-rust","category":"anthropic-skill","format":"markdown","content":"# write-script-rust\n\nPath: `system_prompts/auto-generated/skills/write-script-rust/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-rust/SKILL.md)","tokens":300,"sizeBytes":249},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md","title":"write-script-snowflake","category":"anthropic-skill","format":"markdown","content":"# write-script-snowflake\n\nPath: `system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-script-snowflake/SKILL.md)","tokens":300,"sizeBytes":264},{"name":"SKILL.md","path":"system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md","title":"write-workflow-as-code","category":"anthropic-skill","format":"markdown","content":"# write-workflow-as-code\n\nPath: `system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/system_prompts/auto-generated/skills/write-workflow-as-code/SKILL.md)","tokens":300,"sizeBytes":264},{"name":"CLAUDE.md","path":"ai_evals/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/CLAUDE.md","title":"ai_evals - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# ai_evals - Claude Agent Guidelines & System Prompt\n\nPath: `ai_evals/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/CLAUDE.md)","tokens":300,"sizeBytes":192},{"name":"CLAUDE.md","path":"backend/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/CLAUDE.md","title":"backend - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# backend - Claude Agent Guidelines & System Prompt\n\nPath: `backend/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/CLAUDE.md)","tokens":300,"sizeBytes":189},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/CLAUDE.md)","tokens":300,"sizeBytes":163},{"name":"CLAUDE.md","path":"frontend/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/CLAUDE.md","title":"frontend - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# frontend - Claude Agent Guidelines & System Prompt\n\nPath: `frontend/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/CLAUDE.md)","tokens":300,"sizeBytes":192},{"name":"README.md","path":"backend/generate_mcp_endpoints_tools/README.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/generate_mcp_endpoints_tools/README.md","title":"generate_mcp_endpoints_tools Documentation","category":"plugin-manifest","format":"markdown","content":"# generate_mcp_endpoints_tools Documentation\n\nPath: `backend/generate_mcp_endpoints_tools/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/generate_mcp_endpoints_tools/README.md)","tokens":300,"sizeBytes":240},{"name":"branch-diff-reviewer.md","path":".claude/agents/branch-diff-reviewer.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/branch-diff-reviewer.md","title":"Subagent: branch-diff-reviewer","category":"subagent-persona","format":"markdown","content":"# Subagent: branch-diff-reviewer\n\nPath: `.claude/agents/branch-diff-reviewer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/branch-diff-reviewer.md)","tokens":300,"sizeBytes":212},{"name":"openapi-sync.md","path":".claude/agents/openapi-sync.md","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/openapi-sync.md","title":"Subagent: openapi-sync","category":"subagent-persona","format":"markdown","content":"# Subagent: openapi-sync\n\nPath: `.claude/agents/openapi-sync.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/agents/openapi-sync.md)","tokens":300,"sizeBytes":188},{"name":"config.toml","path":".codex/config.toml","rawUrl":"https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.codex/config.toml","title":"Codex Directive: config.toml","category":"codex-rule","format":"markdown","content":"# Codex Directive: config.toml\n\nPath: `.codex/config.toml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.codex/config.toml)","tokens":300,"sizeBytes":170}],"systemPromptSnippet":"<yakaai_skills repo=\"windmill-labs/windmill\">\n<!-- File: AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/AGENTS.md)\n\n<!-- File: ai_evals/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# ai_evals - AI Agent Protocol & Instructions\n\nPath: `ai_evals/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/ai_evals/AGENTS.md)\n\n<!-- File: backend/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# backend - AI Agent Protocol & Instructions\n\nPath: `backend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/backend/AGENTS.md)\n\n<!-- File: frontend/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# frontend - AI Agent Protocol & Instructions\n\nPath: `frontend/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/frontend/AGENTS.md)\n\n<!-- File: .agents/skills/adding-a-trigger/SKILL.md (Tokens: ~4901 | Category: anthropic-skill) -->\n---\nname: adding-a-trigger\ndescription: 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.\n---\n\n# Skill: Adding a New Trigger Type\n\nUse 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.\n\nThe 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.\n\nThroughout this doc, substitute `{kind}` for the new trigger kind (`azure`, `kafka`, …), `{Kind}` for PascalCase (`Azure`, `Kafka`), `{KIND}` for SCREAMING (`AZURE`, `KAFKA`).\n\n## Reference implementations\n\n- **GCP** — closest analogue to Azure. Has push + pull, OIDC auth, ARM-like resource paths, capture handler. Grep for `gcp_trigger` / `GcpTrigger`.\n- **Kafka** — simpler (pull-only, streaming). Good for trivial integrations.\n- **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`.\n\n## 1. Database migration\n\nCreate a migration: `cargo sqlx migrate add -r add_{kind}_trigger` from `backend/`. Never write timestamps manually.\n\nThe `up.sql` usually defines:\n- An optional enum type (e.g. `AZURE_MODE`) if the trigger has sub-kinds\n- The `{kind}_trigger` table with at minimum these columns (mirrored from kafka/gcp):\n  - primary: `(workspace_id, path)`\n  - `script_path`, `is_flow`, `enabled`, `mode`, `permissioned_as`, `edited_by`, `email`\n  - `edited_at`, `error`, `server_id`, `last_server_ping`\n  - `error_handler_path`, `error_handler_args jsonb`, `retry jsonb`\n  - trigger-specific fields\n- Indexes on foreign keys + any frequently-filtered columns\n- Foreign key to `workspace`\n- The RLS policies (`see_own`, `see_member`, `see_folder_extra_perms_user_*`, `see_extra_perms_user_*`, `see_extra_perms_groups_*`), copied from an existing trigger table\n\n**RLS: wrap every session GUC read in a scalar sub-select.** Write the session\nreads as `(select current_setting('session.user'))`,\n`= any((select regexp_split_to_array(current_setting('session.groups'), ','))::text[])`,\n`?| (select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[]`,\n`? (select concat('u/', current_setting('session.user')))`, etc. — not the bare\n`current_setting(...)`. The GUCs are set with `SET LOCAL`, so the sub-select\nhoists them to a one-time InitPlan instead of re-evaluating per scanned row.\nPut the `::text[]` cast **outside** the sub-select for the array cases: in an\n`= any (...)` context, casting inside — `= any((select ...::text[]))` — makes\nPostgres parse the operand as a row-returning subquery and fails at CREATE with\n`operator does not exist: text = text[]`. The outside cast keeps it in\narray-operand form. See migration `20260714230440_wrap_session_gucs_in_rls_policies`\nfor the canonical wrapped forms.\n\nDown migration drops the table and any enum types.\n\n## 2. Backend crate (`windmill-trigger-{kind}`)\n\nCreate a new crate under `backend/windmill-trigger-{kind}/` with:\n\n- `Cargo.toml`: features `enterprise`, `private` if EE, standard deps\n- `src/lib.rs`: `pub use mod_ee::*;` behind `#[cfg(all(feature = \"enterprise\", feature = \"private\"))]`\n- `src/mod_ee.rs`: core types + helpers\n- `src/handler_ee.rs`: `TriggerCrud` impl + route handlers\n- `src/listener_ee.rs`: (only if streaming/pull-based) `Listener` trait impl\n\nRequired in `mod_ee.rs`:\n- `{Kind}Config` struct (persisted shape, `FromRow`)\n- `{Kind}ConfigRequest` struct (what API receives — usually similar to Config but with validation fields)\n- `{Kind}Trigger` unit struct (implements the traits)\n- `impl TriggerJobArgs for {Kind}Trigger` — sets `TRIGGER_KIND`, `Payload`, `v1_payload_fn`\n\nRequired in `handler_ee.rs`:\n- `#[async_trait] impl TriggerCrud for {Kind}Trigger` with:\n  - `type Trigger = Trigger<{Kind}Config>`\n  - `type TriggerConfigRequest = {Kind}ConfigRequest`\n  - `const ROUTE_PREFIX: &'static str = \"/{kind}_triggers\";`\n  - `const TABLE_NAME`, `ADDITIONAL_SELECT_FIELDS`\n  - `get_deployed_object`, `validate_config`, `create_trigger`, `update_trigger`, `delete_trigger`, `test_connection`\n  - `additional_routes` (optional — mount extra endpoints for things like ARM resource listing, topic discovery)\n\nRegister the crate in `backend/Cargo.toml` as a workspace member and as a dep of `windmill-api` behind the feature flag.\n\n## 3. Wire into `windmill-api` (feature-gated everywhere)\n\n**`backend/windmill-api/src/triggers/handler.rs`** — mount the trigger crate:\n```rust\n#[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n{\n    use crate::triggers::{kind}::{Kind}Trigger;\n    router = router.nest({Kind}Trigger::ROUTE_PREFIX, complete_trigger_routes({Kind}Trigger));\n}\n```\n\n**`backend/windmill-api/src/triggers/{kind}/mod.rs`** — re-export the crate:\n```rust\npub use windmill_trigger_{kind}::*;\n```\n\n**`backend/windmill-api/src/lib.rs`** — if the trigger receives inbound pushes, add a webhook route:\n```rust\n.nest(\"/{kind}/w/{workspace_id}\", {\n    #[cfg(all(feature = \"enterprise\", feature = \"{kind}_trigger\", feature = \"private\"))]\n    { triggers::{kind}::handler_oss::{kind}_push_route_handler() }\n    #[cfg(not(...))]\n    { Router::new() }\n})\n```\n\n## 4. `TriggerKind` enum (`backend/windmill-types/src/triggers.rs`)\n\nAlready has slots for most triggers but verify your variant exists:\n- Add `{Kind}` to the `TriggerKind` enum\n- Add match arm in `to_key()`\n- Add match arm in `from_str`\n- Add match arm in `JobTriggerKind` (if jobs need kind tagging)\n\n## 5. OpenAPI (`backend/windmill-api/openapi.yaml`)\n\nThis file is huge and the single most-forgotten place. Add:\n\n- `/w/{workspace}/{kind}_triggers/create` + `/update/{path}` + `/delete/{path}` + `/get/{path}` + `/list` + `/exists/{path}` + `/setmode/{path}` + `/test` paths (mirror gcp section)\n- Any `additional_routes` your handler exposes (resource discovery, etc.)\n- Schemas: `{Kind}Trigger`, `{Kind}TriggerData`, `{Kind}Mode` (if enum), `{Kind}DeliveryConfig`, helper request/response types\n- Add `{kind}` to `CaptureTriggerKind` enum\n- Add `{kind}_used: boolean` to the `UsedTriggers` response schema\n\nRegenerate frontend client: `npm run generate-backend-client` from `frontend/`.\n\n## 6. `UsedTriggers` + workspace export\n\n**`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.\n\n**`backend/windmill-api/src/workspaces_export.rs`** — add export block mirroring gcp's (export lists all triggers, serializes them to YAML/JSON). The block re-uses the `trigger_ignore_keys` variable so the new kind automatically participates in fork-export stripping (`mode` field is omitted when the source workspace is a fork — keeps fork→parent merges from flipping the parent's enabled state).\n\n**Fork cloning (`clone_triggers_and_schedules` in workspaces.rs)** — add an `INSERT INTO {kind}_trigger ... SELECT ...` block that copies all rows from the parent workspace, forcing `mode = 'disabled'::TRIGGER_MODE`. Always runs at fork creation; forgetting this means users can't carry `{kind}` triggers into their forks.\n\n## 6.5 Hardcoded trigger-kind arrays (silent-failure hotspots)\n\nSeveral files keep **hardcoded arrays** of trigger kind strings. Miss one and ACL checks / user offboarding / trash drop your kind:\n\n- **`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.\n- **`backend/windmill-api-users/src/users.rs`** (`extra_perms_tables`) — which tables get `extra_perms` entries cleaned when a user is deleted.\n- **`backend/windmill-api/src/offboarding.rs`** — three separate arrays (enumeration, fork-copy, and delete paths). **All three** need the new kind.\n- **`backend/windmill-api/src/trash.rs`** — `valid_tables` for the trash / restore API.\n- **`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).\n- **`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.\n- **`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.\n\n**OpenAPI enums** to extend (do NOT forget — generated client will allow it but server rejects as 400):\n- `CaptureTriggerKind` enum\n- Three `kind` enums under `/w/{workspace}/acls/{get,add,remove}/{kind}/{path}` (yes, same list repeated three times)\n\nAfter 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.\n\n## 7. Capture infrastructure (`backend/windmill-api/src/capture.rs`)\n\nIf the trigger supports push delivery, it also needs a capture endpoint so users can test it:\n\n- `{Kind}TriggerConfig` struct (gated by feature flags)\n- `TriggerConfig::{Kind}` variant\n- `set_{kind}_trigger_config` function (creates the subscription/equivalent pointing at the capture URL — use your `manage_{kind}_subscription` helper with `trigger_mode=false`)\n- Both real + no-op versions behind feature gates\n- `TriggerKind::{Kind} => set_{kind}_trigger_config(...)` arm in `set_config`\n- `{kind}_payload` async handler — validates auth (if any), processes payload, calls `insert_capture_payload`\n- Route: `.route(\"/{kind}/{runnable_kind}/{*path}\", post({kind}_payload))` inside `workspaced_unauthed_service` — and expand the surrounding `#[cfg(any(...))]` to include your feature flag\n\n## 8. CLI (`cli/`) — easy to miss, breaks sync silently\n\nCheck all of these:\n\n**`cli/src/types.ts`:**\n- Add `\"{kind}\"` to `TRIGGER_TYPES` array\n- Add `\"{kind}_trigger\"` to `getTypeStrFromPath` return union\n- Add match case in `getTypeStrFromPath`'s `typeEnding ===` chain\n- Add `pushTrigger(\"{kind}\", ...)` branch in `pushObj`\n\n**`cli/src/commands/trigger/trigger.ts`:**\n- Import `{Kind}Trigger` type\n- Add `{kind}: {Kind}Trigger` to the `Trigger` type map\n- Add `{kind}: wmill.get{Kind}Trigger`, `update{Kind}Trigger`, `create{Kind}Trigger` to each function map\n- Add `{kind}: { ... }` template to `triggerTemplates`\n- Add `list{Kind}Triggers` call + spread in the `list` aggregation\n- Update `--kind` option descriptions to mention the new kind\n\n**`cli/src/commands/sync/sync.ts`:**\n- Add `path.endsWith(\".{kind}_trigger\" + ext)` in the file-type filter\n- Add `typ == \"{kind}_trigger\"` in `getTypeOrder`\n- Add `\"{kind}_trigger\"` to the delete-suffix regex (~line 3092)\n- Add a `case \"{kind}_trigger\"` in the delete switch\n\n**`cli/src/guidance/skills.ts`** — **DO NOT EDIT DIRECTLY**. It's auto-generated by `system_prompts/generate.py`. Instead:\n- 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)\n- Then run `python3 system_prompts/generate.py` — it regenerates `cli/src/guidance/skills.ts` with the schema extracted from `backend/windmill-api/openapi.yaml`\n- Commit the regenerated file\n\n## 9. Frontend — editor + drawer\n\nUnder `frontend/src/lib/components/triggers/{kind}/`:\n\n- `{Kind}TriggerPanel.svelte` — the tile shown in the triggers listing\n- `{Kind}TriggerEditor.svelte` — outer drawer wrapper\n- `{Kind}TriggerEditorInner.svelte` — state + business logic; must expose:\n  - `openEdit(path, isFlow, defaultValues?)` method\n  - `isEditor` prop, `onConfigChange` + `onCaptureConfigChange` callbacks\n  - `get{Kind}Config()` + `get{Kind}CaptureConfig()` helpers\n  - `captureConfig = $derived.by(untrack(() => isEditor) ? get{Kind}CaptureConfig : () => ({}))`\n  - `$effect(() => { const args = [captureConfig, isValid] as const; untrack(() => onCaptureConfigChange?.(...args)) })`\n- `{Kind}TriggerEditorConfigSection.svelte` — form fields; use design-system components (`TextInput`, `Select`, `Toggle`, `ToggleButtonGroup`), never raw `<input>`\n- `{Kind}Capture.svelte` — capture panel; wraps `CaptureSection` with `captureType=\"{kind}\"`\n- `utils.ts` — `requestBody` builders and any trigger-type-specific helpers\n\n## 10. Frontend — global integration\n\nEasy to miss:\n\n- **`frontend/src/lib/components/triggers.ts`** — add `'{kind}'` to the `TriggerKind` union\n- **`frontend/src/lib/components/triggers/CaptureWrapper.svelte`**:\n  - Import `{Kind}Capture`\n  - Add to `isStreamingCapture()` array (streaming = pull-style; push-style is typically `false`)\n  - Add `{:else if captureType === '{kind}'}` branch with the `<{Kind}Capture>` render\n- **`frontend/src/lib/components/sidebar/SidebarContent.svelte`** — import the icon, add the nav entry\n- **`frontend/src/lib/components/sidebar/OperatorMenu.svelte`** — add the operator-mode entry\n- **`frontend/src/routes/(root)/(logged)/+layout.svelte`** — destructure `{kind}_used` from `/get_used_triggers` response, push `'{kind}'` into `usedKinds`\n- **`frontend/src/lib/components/search/GlobalSearchModal.svelte`** — import icon, add \"Go to {Kind} ...\" entry\n- **`frontend/src/lib/components/offboarding-utils.ts`** — add mappings `{kind}_trigger: '{kind}_triggers'` and `{kind}_trigger: '{kind} trigger'`\n- **`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)\n- **`frontend/src/routes/(root)/(logged)/{kind}_triggers/+page.svelte`** — listing page (mirror `gcp_triggers/+page.svelte` for push+pull, `kafka_triggers` for pure streaming)\n- **`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\n\n## 10.5 AI system prompts (`system_prompts/`)\n\n- **`system_prompts/utils.py`** — append `('{Kind}Trigger', '{kind}_trigger')` to `SCHEMA_MAPPINGS['triggers']` (master list used by code generation + CLI skills)\n- **`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\n- **`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)\n- Run `python3 system_prompts/generate.py` — this rewrites `cli/src/guidance/skills.ts` and all `auto-generated/` docs. Commit the regenerated files\n\n## 11. Validation\n\nRun all of these before declaring done:\n\n```bash\n# Backend\ncd backend\ncargo check --features enterprise,{kind}_trigger,private   # minimal\ncargo check --features enterprise,azure_trigger,private,gcp_trigger,http_trigger,mqtt_trigger,postgres_trigger,sqs_trigger,kafka,nats,smtp,websocket  # full\n\n# SQLx offline data (never run `cargo sqlx prepare` directly — use the wrapper)\n./update_sqlx.sh\n\n# Frontend\ncd frontend\nnpm run generate-backend-client\nnpm run check:fast\n```\n\nSmoke test in the UI: create a trigger, save, check it appears in sidebar + search, delete, re-create via CLI `wmill sync`.\n\n## 12. Common pitfalls\n\n- **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\n- **`.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`\n- **`on:event` directives** — legacy Svelte 4, no-op in runes mode. Use callback props (`onSelected`, `onConfigChange`)\n- **`$bindable(default_value)` on optional props** — banned by project CLAUDE.md. Use `$bindable()` + `$derived(prop ?? default)` instead\n- **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)\n- **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\n- **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\n- **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\n- **Workspace-scoped tag compatibility** — if the trigger has tags, verify forked workspaces handle them (see commit `0773b5bc85` for a historical fix)\n\n## 13. EE file split\n\nIf 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.\n\n## 14. Final checklist before PR\n\n- [ ] Migration up/down tested (revert + re-apply)\n- [ ] `./update_sqlx.sh` committed the updated `.sqlx/` offline data\n- [ ] `cargo check` passes with your feature flag + with all trigger features\n- [ ] `npm run check:fast` passes\n- [ ] Trigger visible in sidebar with correct icon weight (not oversized/colored — use `currentColor`)\n- [ ] Create, edit, delete flow all work in the UI\n- [ ] Capture button works (if push-capable)\n- [ ] Trigger appears in `/get_used_triggers` → sidebar pulse\n- [ ] `wmill sync pull` + `wmill sync push` both round-trip the trigger\n- [ ] `wmill trigger list` includes it\n- [ ] OpenAPI schemas are complete (no `null` in generated types)\n\n<!-- File: .agents/skills/ai-chat/SKILL.md (Tokens: ~606 | Category: anthropic-skill) -->\n---\nname: ai-chat\ndescription: Guidance for improving the Windmill AI chat (copilot), especially global mode — tools, prompts, and context-window discipline. Use when editing chat tools, system prompts, or tool-result shapes under frontend/src/lib/components/copilot/chat, or when changing how the chat manages its context window.\n---\n\n## Always benchmark before and after\n\nNo context or behavior change ships without an `ai_evals` A/B on the affected mode.\nAdd or adjust cases for exactly what you changed — see the `ai-evals` skill for\nauthoring and the full run reference.\n\nRun the affected mode **before** your change and **after**, same model(s), same cases.\n\n## Where guidance goes\n\nWhat the model should know about Windmill itself (flow shapes, groups, data tables, app\naccess, secrets…) belongs in `system_prompts/base/` or `system_prompts/languages/`, which also\nfeed the CLI's skills — not in a `*/core.ts` prompt builder, where only this chat would see it.\nThe builders add tool plumbing and runtime values only. Scope a sentence to one consumer with\n`<!-- chat-only -->` / `<!-- cli-only -->`, keep chat tool names out of the shared files, and\nrerun `python system_prompts/generate.py`. `system_prompts/README.md` has the mechanics.\n\n## Measure the window first, and cumulative second\n\nOptimize **`finalContextTokens`** (window occupancy — what drives overflow and\ncompaction), then cumulative prompt tokens.\n\n## Context discipline\n\nThe dominant fixed cost is per-iteration overhead: the system prompt **plus every\ntool schema** is re-sent on every loop iteration. So:\n\n- **Every tool and every parameter is a permanent tax.** Justify each one and measure\n  it; an extra \"locate\" round-trip can cost more than the reads it saves. Strip dead\n  params rather than leaving them in the schema.\n- **Tool results return the minimum.** Never echo content the model already has. The\n  canonical mistake: a write tool that returns the whole edited artifact right after\n  the model authored it — return `{ success, message }` instead. When you touch a\n  *shared* write helper (e.g. `finishAppDraftWrite` in `global/core.ts`), re-check\n  this invariant for **all** the write tools routing through it — the echo has\n  regressed before via a shared refactor.\n\n## Prompts and tool descriptions are part of the surface\n\nThe system prompt and tool descriptions steer behavior as much as the tools\nthemselves, and are benchmarkable the same way. A description that advertises\ntruncation makes the model self-limit; the path-conventions block changes where\ndrafts land. Treat prompt/description edits as real changes and A/B them — a\npure-prompt change is a legitimate, measurable improvement.\n\n<!-- File: .agents/skills/ai-evals/SKILL.md (Tokens: ~894 | Category: anthropic-skill) -->\n---\nname: ai-evals\ndescription: Author and run black-box benchmark cases for the Windmill AI generation modes (flow/app/script/cli/global) in ai_evals/. Use when adding or changing eval cases, or when running before/after benchmarks for AI chat / copilot changes.\n---\n\n# AI evals — authoring and running benchmark cases\n\n`ai_evals/` is a black-box benchmark runner for the Windmill AI generation modes:\n`flow`, `app`, `script`, `cli`, `global`. It always tests the **current** production\nprompts, tools, and guidance in this checkout. Each attempt runs the real production\npath, deterministic validation, then LLM judging.\n\nThe goal is to test current production guidance with realistic user requests — **not**\nto pin one exact implementation shape.\n\n## Running benchmarks\n\n```bash\ncd ai_evals\nbun install                       # first time; frontend modes also need `cd frontend && bun install`\nbun run cli -- models             # list model aliases\nbun run cli -- cases global       # list cases for a mode\nbun run cli -- run global global-test1-script-create --model sonnet\n```\n\nFrontend modes (`flow`/`script`/`app`/`global`) route model calls through a Windmill\nbackend's `/api/w/<ws>/ai/proxy`, so you need **any** reachable backend:\n\n```bash\nWMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:<port> WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests \\\n  bun run cli -- run global <caseIds...> --models sonnet,gpt-5.5,gemini-3.1-pro-preview\n```\n\n- **Reuse an existing workspace.** CE builds cap workspaces, so temp-workspace\n  creation 400s (\"reached workspace limit\"). Always set\n  `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` (or any existing workspace) to\n  reuse one. The only side effect of a run is upserting an `f/evals/ai/<provider>`\n  resource there.\n- Provider keys live in `ai_evals/.env` and are auto-loaded by bun. The judge is a\n  separate Anthropic call (default `claude-sonnet-4-6`) regardless of the model under\n  test.\n\n## Authoring core rules\n\n1. Write prompts like a real user request.\n2. Prefer behavior, inputs, constraints, and outcomes over internal implementation.\n3. Keep deterministic validation narrow and hard.\n4. Put semantic expectations in `judgeChecklist`.\n5. Use `expected` fixtures only when exact structure really matters.\n\n### Prompt writing\n\nPrompts should sound like something a user would naturally ask. Do not write prompts\nas if the user knows Windmill internals unless the case explicitly tests a power-user\nworkflow.\n\nGood:\n- \"Create a flow that routes support requests based on customer tier.\"\n- \"Add a reset button that sets the counter back to 0.\"\n- \"Create a flow that reuses the existing greeting script instead of duplicating the logic.\"\n\nBad:\n- \"Use `branchone` with 3 branches and a default branch.\"\n- \"Create a `rawscript` step with this exact topology.\"\n- \"This is a benchmark harness.\"\n\n### Deterministic validation\n\nUse deterministic checks only for hard failures: missing required files; unexpected\nextra files when the prompt says not to create them; syntax errors; unresolved flow\nrefs; missing required special modules or suspend config; obvious corruption.\n\nDo **not** encode one preferred implementation. Bad hard checks: exact step topology\nfor a creation flow; exact branch structure when the prompt only asked for routing;\nexact input shape when multiple reasonable shapes are acceptable.\n\n### Judge checklist\n\nEvery non-trivial case should have a `judgeChecklist` capturing user-visible behavior\nthat must be present, important constraints, and key completion criteria — not\nlow-level implementation details unless truly required.\n\nGood: \"the flow calculates the order total with 8% tax\"; \"the flow reuses the existing\nworkspace script instead of rewriting the logic\". Bad: \"uses `branchone`\"; \"contains a\n`rawscript` node\".\n\nSee `ai_evals/README.md` for the full case format, fields, and fixture details.\n\n<!-- File: .agents/skills/codebase-design/DEEPENING.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DEEPENING.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DEEPENING.md)\n\n<!-- File: .agents/skills/codebase-design/DESIGN-IT-TWICE.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Codebase-design Skill\n\nPath: `.agents/skills/codebase-design/DESIGN-IT-TWICE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/codebase-design/DESIGN-IT-TWICE.md)\n\n<!-- File: .agents/skills/codebase-design/SKILL.md (Tokens: ~1379 | Category: anthropic-skill) -->\n---\nname: codebase-design\ndescription: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\n---\n\n# Codebase Design\n\nDesign **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone.\n\n## Glossary\n\nUse these terms exactly — don't substitute \"component,\" \"service,\" \"API,\" or \"boundary.\" Consistent language is the whole point.\n\n**Module** — anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.\n\n**Interface** — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow — they refer only to the type-level surface).\n\n**Implementation** — what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for \"adapter\" when the seam is the topic; \"implementation\" otherwise.\n\n**Depth** — leverage at the interface: the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.\n\n**Seam** _(Michael Feathers)_ — a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).\n\n**Adapter** — a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).\n\n**Leverage** — what callers get from depth: more capability per unit of interface they learn. One implementation pays back across N call sites and M tests.\n\n**Locality** — what maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.\n\n## Deep vs shallow\n\n**Deep module** = small interface + lots of implementation:\n\n```\n┌─────────────────────┐\n│   Small Interface   │  ← Few methods, simple params\n├─────────────────────┤\n│                     │\n│  Deep Implementation│  ← Complex logic hidden\n│                     │\n└─────────────────────┘\n```\n\n**Shallow module** = large interface + little implementation (avoid):\n\n```\n┌─────────────────────────────────┐\n│       Large Interface           │  ← Many methods, complex params\n├─────────────────────────────────┤\n│  Thin Implementation            │  ← Just passes through\n└─────────────────────────────────┘\n```\n\nWhen designing an interface, ask:\n\n- Can I reduce the number of methods?\n- Can I simplify the parameters?\n- Can I hide more complexity inside?\n\n## Principles\n\n- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.\n- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.\n- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.\n- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.\n\n## Designing for testability\n\nGood interfaces make testing natural:\n\n1. **Accept dependencies, don't create them.**\n\n   ```typescript\n   // Testable\n   function processOrder(order, paymentGateway) {}\n\n   // Hard to test\n   function processOrder(order) {\n     const gateway = new StripeGateway();\n   }\n   ```\n\n2. **Return results, don't produce side effects.**\n\n   ```typescript\n   // Testable\n   function calculateDiscount(cart): Discount {}\n\n   // Hard to test\n   function applyDiscount(cart): void {\n     cart.total -= discount;\n   }\n   ```\n\n3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.\n\n## Relationships\n\n- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).\n- **Depth** is a property of a **Module**, measured against its **Interface**.\n- A **Seam** is where a **Module**'s **Interface** lives.\n- An **Adapter** sits at a **Seam** and satisfies the **Interface**.\n- **Depth** produces **Leverage** for callers and **Locality** for maintainers.\n\n## Rejected framings\n\n- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.\n- **\"Interface\" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.\n- **\"Boundary\"**: overloaded with DDD's bounded context. Say **seam** or **interface**.\n\n## Going deeper\n\n- **Deepening a cluster given its dependencies** — see `.agents/skills/codebase-design/DEEPENING.md` (path from the repo root): dependency categories, seam discipline, and replace-don't-layer testing.\n- **Exploring alternative interfaces** — see `.agents/skills/codebase-design/DESIGN-IT-TWICE.md` (path from the repo root): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement.\n\n<!-- File: .agents/skills/commit/SKILL.md (Tokens: ~436 | Category: anthropic-skill) -->\n---\nname: commit\nuser_invocable: true\ndescription: Create a git commit with conventional commit format. MUST use anytime you want to commit changes.\n---\n\n# Git Commit Skill\n\nCreate a focused, single-line commit following conventional commit conventions.\n\n## Instructions\n\n1. **Analyze changes**: Run `git status` and `git diff` to understand what was modified\n2. **Stage only modified files**: Add files individually by name. NEVER use `git add -A` or `git add .`\n3. **Write commit message**: Follow the conventional commit format as a single line\n\n## Conventional Commit Format\n\n```\n<type>: <description>\n```\n\n### Types\n- `feat`: New feature or capability\n- `fix`: Bug fix\n- `refactor`: Code change that neither fixes a bug nor adds a feature\n- `docs`: Documentation only changes\n- `style`: Formatting, missing semicolons, etc (no code change)\n- `test`: Adding or correcting tests\n- `chore`: Maintenance tasks, dependency updates, etc\n- `perf`: Performance improvement\n\n### Rules\n- Message MUST be a single line (no multi-line messages)\n- Description should be lowercase, imperative mood (\"add\" not \"added\")\n- No period at the end\n- Keep under 72 characters total\n\n### Examples\n```\nfeat: add token usage tracking for AI providers\nfix: resolve null pointer in job executor\nrefactor: extract common validation logic\ndocs: update API endpoint documentation\nchore: upgrade sqlx to 0.7\n```\n\n## Execution Steps\n\n1. Run `git status` to see all changes\n2. Run `git diff` to understand the changes in detail\n3. Run `git log --oneline -5` to see recent commit style\n4. Stage ONLY the modified/relevant files: `git add <file1> <file2> ...`\n5. Create the commit with conventional format:\n   ```bash\n   git commit -m \"<type>: <description>\"\n   ```\n6. Run `git status` to verify the commit succeeded\n\n<!-- File: .agents/skills/domain-modeling/CONTEXT-FORMAT.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Domain-modeling Skill\n\nPath: `.agents/skills/domain-modeling/CONTEXT-FORMAT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/domain-modeling/CONTEXT-FORMAT.md)\n\n<!-- File: .agents/skills/domain-modeling/SKILL.md (Tokens: ~554 | Category: anthropic-skill) -->\n---\nname: domain-modeling\ndescription: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, or when another skill needs to maintain the domain model.\n---\n\n# Domain Modeling\n\nActively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)\n\n## File structure\n\nMost repos have a single context:\n\n```\n/\n├── CONTEXT.md\n└── src/\n```\n\nIf a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:\n\n```\n/\n├── CONTEXT-MAP.md\n└── src/\n    ├── ordering/\n    │   └── CONTEXT.md\n    └── billing/\n        └── CONTEXT.md\n```\n\nCreate files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved.\n\n## During the session\n\n### Challenge against the glossary\n\nWhen the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. \"Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?\"\n\n### Sharpen fuzzy language\n\nWhen the user uses vague or overloaded terms, propose a precise canonical term. \"You're saying 'account' — do you mean the Customer or the User? Those are different things.\"\n\n### Discuss concrete scenarios\n\nWhen domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.\n\n### Cross-reference with code\n\nWhen the user states how something works, check whether the code agrees. If you find a contradiction, surface it: \"Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?\"\n\n### Update CONTEXT.md inline\n\nWhen a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in `.agents/skills/domain-modeling/CONTEXT-FORMAT.md` (path from the repo root).\n\n`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.\n\n<!-- File: .agents/skills/grill-me/SKILL.md (Tokens: ~34 | Category: anthropic-skill) -->\n---\nname: grill-me\ndescription: A relentless interview to sharpen a plan or design.\ndisable-model-invocation: true\n---\n\nRun a `/grilling` session.\n\n<!-- File: .agents/skills/grilling/SKILL.md (Tokens: ~395 | Category: anthropic-skill) -->\n---\nname: grilling\ndescription: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n---\n\nInterview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.\n\nWork the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.\n\nEach question should be formatted like so:\n\n```\n❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>\n\n➡️ <your recommended answer>\n```\n\nEach round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.\n\nFinding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it — don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report — ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait.\n\nThe session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.\n\n<!-- File: .agents/skills/improve-codebase-architecture/HTML-REPORT.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Improve-codebase-architecture Skill\n\nPath: `.agents/skills/improve-codebase-architecture/HTML-REPORT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/improve-codebase-architecture/HTML-REPORT.md)\n\n<!-- File: .agents/skills/improve-codebase-architecture/SKILL.md (Tokens: ~1168 | Category: anthropic-skill) -->\n---\nname: improve-codebase-architecture\ndescription: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.\ndisable-model-invocation: true\n---\n\n# Improve Codebase Architecture\n\nSurface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.\n\nThis command is _informed_ by the project's domain model and built on a shared design vocabulary:\n\n- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, \"the interface is the test surface\", \"one adapter = hypothetical seam, two = real\"). Use these terms exactly in every suggestion — don't drift into \"component,\" \"service,\" \"API,\" or \"boundary.\"\n- The domain language in `CONTEXT.md` gives names to good seams.\n\n## Process\n\n### 1. Explore\n\n**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:\n\n- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.\n- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.\n\nRead the project's domain glossary (`CONTEXT.md`) first.\n\nThen spawn a sub-agent to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:\n\n- Where does understanding one concept require bouncing between many small modules?\n- Where are modules **shallow** — interface nearly as complex as the implementation?\n- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?\n- Where do tightly-coupled modules leak across their seams?\n- Which parts of the codebase are untested, or hard to test through their current interface?\n\nApply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A \"yes, concentrates\" is the signal you want.\n\n### 2. Present candidates as an HTML report\n\nWrite a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `<tmpdir>/architecture-review-<timestamp>.html` so each run gets a fresh file. Open it for the user — `xdg-open <path>` on Linux, `open <path>` on macOS, `start <path>` on Windows — and tell them the absolute path.\n\nThe report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.\n\nFor each candidate, render a card with:\n\n- **Files** — which files/modules are involved\n- **Problem** — why the current architecture is causing friction\n- **Solution** — plain English description of what would change\n- **Benefits** — explained in terms of locality and leverage, and how tests would improve\n- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening\n- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge\n\nEnd the report with a **Top recommendation** section: which candidate you'd tackle first and why.\n\n**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines \"Order,\" talk about \"the Order intake module\" — not \"the FooBarHandler,\" and not \"the Order service.\"\n\nSee `.agents/skills/improve-codebase-architecture/HTML-REPORT.md` (path from the repo root) for the full HTML scaffold, diagram patterns, and styling guidance.\n\nDo NOT propose interfaces yet. After the file is written, ask the user: \"Which of these would you like to explore?\"\n\n### 3. Grilling loop\n\nOnce the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.\n\nSide effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:\n\n- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist.\n- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.\n- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.\n\n<!-- File: .agents/skills/local-review-codex/SKILL.md (Tokens: ~761 | Category: anthropic-skill) -->\n---\nname: local-review-codex\ndescription: Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy, model and reasoning effort as the codex-pr-review GitHub action.\n---\n\n# Local Codex Review (pre-push)\n\nRuns the exact same review Codex performs in CI (`.github/workflows/codex-pr-review.yml`),\nbut locally and scoped to work you have not pushed yet — so you catch what CI would flag\nbefore the PR exists. Use this before `git push` on a non-trivial change.\n\n**Correspondence with CI** — identical:\n- Policy: `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test coverage).\n- Model: `gpt-6.1-sol`.\n- Reasoning effort: `model_reasoning_effort=\"xhigh\"`.\n- Output: markdown starting with `## Codex Review`, findings tagged P0 / P1 / P2 with file:line.\n\n**Differences from CI** — local-only:\n- Scope is the current branch vs `main` at the merge-base, **including uncommitted changes** (CI reviews a pushed PR diff).\n- Sandbox is `read-only` (CI uses `danger-full-access` on an ephemeral runner). Codex reads the diff and files but cannot modify your working tree.\n- Fresh context is inherent: `codex exec` is a separate cold process, so it does not anchor on the current chat session — the same reason `local-review` insists on a subagent.\n\n## Prerequisites\n\n- `codex` CLI **>= 0.159.3** installed and authed via `codex login` or an `OPENAI_API_KEY` in the environment (which takes priority). Older CLIs reject the model with \"requires a newer version of Codex\"; `run.sh` checks the version up front. Upgrade with `npm install --global @openai/codex@0.159.3` (may need `sudo` for a global install). This matches the pin in `.github/workflows/codex-pr-review.yml` — the CLI version is the same on both sides.\n- `git fetch` the base ref if it's stale, so the merge-base is accurate.\n\n## Run\n\n```bash\nbash .agents/skills/local-review-codex/run.sh          # review vs main (default)\nbash .agents/skills/local-review-codex/run.sh <base>   # review vs a different base ref\n```\n\nInvoke with `bash` (or run the executable directly) — the script needs Bash for\n`set -o pipefail`; `sh` is Dash on Debian/Ubuntu and would fail. If `main` isn't a\nlocal branch (e.g. a fresh single-branch checkout), the runner falls back to\n`origin/main` automatically.\n\nThe script computes `BASE_SHA = git merge-base HEAD <base>`, feeds Codex `REVIEW.md` plus a\ndiff context pointing at `git diff <BASE_SHA>` (which folds in uncommitted edits), and prints\nthe review. It writes only temp files — nothing lands in the working tree.\n\n## Relaying the result\n\nPrint the Codex output verbatim. Do not re-summarize or filter it — the value of a cold Codex\npass is surfacing what the current session would rationalize away. Then decide with the user\nwhether to address findings before pushing.\n\nFor a Claude-native review instead, use `local-review` (branch-diff-reviewer subagent). This\nskill is the Codex counterpart; run both for independent perspectives.\n\n<!-- File: .agents/skills/local-review/SKILL.md (Tokens: ~988 | Category: anthropic-skill) -->\n---\nname: local-review\ndescription: Code review the current PR (or branch diff against main) for bugs, security, and AGENTS.md compliance. MUST use when asked to review code.\n---\n\n# Local Code Review\n\nRun the same review locally that the GitHub auto-review actions run on PRs (Claude / Codex / Pi). The review policy lives in `REVIEW.md`.\n\n**Why a subagent**: the review MUST run in a fresh context — not inline in the current session. If the user has been iterating on the diff, the main session has absorbed their reasoning and rationalizations, so it anchors and misses things CI catches. A subagent starts cold, like CI does.\n\n## Steps\n\n1. **Determine the PR scope** (cheap, do this in the main session):\n   - If an argument is provided, treat it as a PR number or branch.\n   - Otherwise, detect from the current branch vs `main`.\n   - Confirm the PR/branch exists (`gh pr view <n>` or `git rev-parse <branch>`).\n\n2. **Delegate the review to a fresh-context subagent** with a self-contained prompt. The prompt MUST include:\n   - The PR number or branch name to review.\n   - The instruction to read `REVIEW.md` first for the policy, then `AGENTS.md` files in directories touched by the diff.\n   - The exact output format (see below).\n   - Whether `--comment` was requested (so the subagent emits inline-comment payloads if needed).\n   - Any \"Additional reviewer instructions\" the user provided.\n\n   - **Claude Code**: use the `Agent` tool with `subagent_type: branch-diff-reviewer` (read-only tools, purpose-built for this). If unavailable, fall back to `general-purpose`.\n   - **Codex / Pi**: if the CLI exposes a fresh-session subagent mechanism, use it. Otherwise tell the user to run the skill in a fresh CLI session and stop — running inline in the current session defeats the purpose.\n\n3. **Receive the findings** from the subagent and relay them to the user verbatim. Do not re-summarize, re-judge, or filter — the whole point of fresh context is to surface what the main session would dismiss.\n\n4. **Post comments if `--comment` was requested**: use the `gh` commands below with the subagent's output as the body. The main session does the posting because the subagent is read-only.\n\n## Subagent prompt template\n\n```\nReview <PR #N | branch X> against main per the policy in REVIEW.md.\n\nSteps:\n1. Read REVIEW.md (repo root) for the full policy: severity triage, public-surface\n   checklist, AGENTS.md compliance, test coverage assessment.\n2. Read AGENTS.md (repo root) and any AGENTS.md in directories touched by the diff.\n3. Get the diff: `gh pr diff <N>` (if PR) or `git diff main...<branch>`.\n4. Get context: `gh pr view <N>` (if PR) or `git log main..<branch> --oneline`.\n5. Read changed files only when the diff alone is insufficient to validate a finding.\n6. Self-validate each finding: \"is this definitely a real issue a senior engineer\n   would flag?\" Discard if uncertain.\n7. Output findings in the exact format below. Do not modify any files.\n\n<paste output format from below>\n\n<if --comment requested:>\nAdditionally emit a JSON array of inline comments suitable for the GitHub reviews\nAPI, one per finding that maps to a specific line:\n[{\"path\": \"...\", \"line\": N, \"side\": \"RIGHT\", \"body\": \"[P1] ...\"}, ...]\n```\n\n## Output format\n\n```\n## Code review\n\n<verdict line per REVIEW.md>\n\nFound N issues:\n\n1. [P0|P1|P2] <description>\n   <file_path:line_number>\n\n2. [P0|P1|P2] <description>\n   <file_path:line_number>\n```\n\nEnd with a `Test coverage` section per the shared policy.\n\nIf no issues are found:\n\n```\n## Code review\n\nGood to merge.\n\nNo issues found. Checked for bugs, security, and AGENTS.md compliance.\n```\n\n## Posting comments (`--comment`)\n\nFor a top-level PR comment:\n\n```bash\ngh pr review --comment --body \"<summary from subagent>\"\n```\n\nFor inline comments on specific lines (using the JSON the subagent emitted):\n\n```bash\ngh api repos/{owner}/{repo}/pulls/{pr}/reviews \\\n  -f body=\"<summary>\" -f event=\"COMMENT\" -f comments=\"<json from subagent>\"\n```\n\n<!-- File: .agents/skills/native-trigger/SKILL.md (Tokens: ~7673 | Category: anthropic-skill) -->\n---\nname: native-trigger\ndescription: Guidance for adding native trigger services to Windmill. Use when implementing or modifying native trigger integrations across the backend and frontend.\n---\n\n# Skill: Adding Native Trigger Services\n\nThis skill provides comprehensive guidance for adding new native trigger services to Windmill. Native triggers allow external services (like Nextcloud, Google Drive, etc.) to trigger Windmill scripts/flows via webhooks or push notifications.\n\n## Architecture Overview\n\nThe native trigger system consists of:\n\n1. **Database Layer** - PostgreSQL tables and enum types\n2. **Backend Rust Implementation** - Core trait, handlers, and service modules in the `windmill-native-triggers` crate\n3. **Frontend Svelte Components** - Configuration forms and UI components\n\n### Key Files\n\n| Component | Path |\n|-----------|------|\n| Core module with `External` trait | `backend/windmill-native-triggers/src/lib.rs` |\n| Generic CRUD handlers | `backend/windmill-native-triggers/src/handler.rs` |\n| Background sync logic | `backend/windmill-native-triggers/src/sync.rs` |\n| OAuth/workspace integration | `backend/windmill-native-triggers/src/workspace_integrations.rs` |\n| Re-export shim (windmill-api) | `backend/windmill-api/src/native_triggers/mod.rs` |\n| TriggerKind enum | `backend/windmill-common/src/triggers.rs` |\n| JobTriggerKind enum | `backend/windmill-common/src/jobs.rs` |\n| Frontend service registry | `frontend/src/lib/components/triggers/native/utils.ts` |\n| Frontend trigger utilities | `frontend/src/lib/components/triggers/utils.ts` |\n| Trigger badges (icons + counts) | `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte` |\n| Workspace integrations UI | `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte` |\n| OAuth config form component | `frontend/src/lib/components/workspaceSettings/OAuthClientConfig.svelte` |\n| OpenAPI spec | `backend/windmill-api/openapi.yaml` |\n| Reference: Nextcloud module | `backend/windmill-native-triggers/src/nextcloud/` |\n| Reference: Google module | `backend/windmill-native-triggers/src/google/` |\n\n### Crate Structure\n\nThe native trigger code lives in the `windmill-native-triggers` crate (`backend/windmill-native-triggers/`). The `windmill-api` crate re-exports everything via a shim:\n\n```rust\n// backend/windmill-api/src/native_triggers/mod.rs\npub use windmill_native_triggers::*;\n```\n\nAll new service modules go in `backend/windmill-native-triggers/src/`.\n\n---\n\n## Core Concepts\n\n### The `External` Trait\n\nEvery native trigger service implements the `External` trait defined in `lib.rs`:\n\n```rust\n#[async_trait]\npub trait External: Send + Sync + 'static {\n    // Associated types:\n    type ServiceConfig: Debug + DeserializeOwned + Serialize + Send + Sync;\n    type TriggerData: Debug + Serialize + Send + Sync;\n    type OAuthData: DeserializeOwned + Serialize + Clone + Send + Sync;\n    type CreateResponse: DeserializeOwned + Send + Sync;\n\n    // Constants:\n    const SUPPORT_WEBHOOK: bool;\n    const SERVICE_NAME: ServiceName;\n    const DISPLAY_NAME: &'static str;\n    const TOKEN_ENDPOINT: &'static str;\n    const REFRESH_ENDPOINT: &'static str;\n    const AUTH_ENDPOINT: &'static str;\n\n    // Required methods:\n    async fn create(&self, w_id, oauth_data, webhook_token, data, db, tx) -> Result<Self::CreateResponse>;\n    async fn update(&self, w_id, oauth_data, external_id, webhook_token, data, db, tx) -> Result<serde_json::Value>;\n    async fn get(&self, w_id, oauth_data, external_id, db, tx) -> Result<Self::TriggerData>;\n    async fn delete(&self, w_id, oauth_data, external_id, db, tx) -> Result<()>;\n    async fn exists(&self, w_id, oauth_data, external_id, db, tx) -> Result<bool>;\n    async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors);\n    fn external_id_and_metadata_from_response(&self, resp) -> (String, Option<serde_json::Value>);\n\n    // Methods with defaults:\n    async fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned>;\n    fn service_config_from_create_response(&self, data, resp) -> Option<serde_json::Value>;\n    fn additional_routes(&self) -> axum::Router;\n    async fn http_client_request<T, B>(&self, url, method, workspace_id, connection_path, db, headers, body) -> Result<T>;\n}\n```\n\nKey design points:\n- **`update()` returns `serde_json::Value`** - the resolved service_config to store. Each service is responsible for building the final config.\n- **`maintain_triggers()`** - periodic background maintenance. Each service implements its own strategy (Nextcloud: reconcile with external state; Google: renew expiring channels).\n- **No `list_all()` in the trait** - services that need it (Nextcloud) implement it privately; services that don't (Google) use different maintenance strategies.\n- **No `get_external_id_from_trigger_data()` or `extract_service_config_from_trigger_data()`** - removed in favor of the `maintain_triggers` pattern.\n\n### Create Lifecycle: Two Paths\n\nThe `create_native_trigger` handler in `handler.rs` supports two creation flows, controlled by `service_config_from_create_response()`:\n\n**Path A: Short (Google pattern)** - `service_config_from_create_response()` returns `Some(config)`:\n1. `create()` registers on external service\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `service_config_from_create_response()` builds the config directly from input data + response metadata\n4. Stores trigger in DB -- done, no extra round-trip\n\nUse this when the external_id is known before the create call (e.g., Google generates the channel_id as a UUID upfront and includes it in the webhook URL).\n\n**Path B: Long (Nextcloud pattern)** - `service_config_from_create_response()` returns `None` (default):\n1. `create()` registers on external service (webhook URL has no external_id yet)\n2. `external_id_and_metadata_from_response()` extracts the ID\n3. `update()` is called to fix the webhook URL with the now-known external_id\n4. `update()` returns the resolved service_config\n5. Stores trigger in DB\n\nUse this when the external_id is assigned by the remote service and the webhook URL needs to be corrected after creation.\n\n### OAuth App and Connections\n\nThe OAuth **app** (client) is configured per workspace and service; **connections** (connected\naccounts) are many per service. Each trigger acts through one connection, named by\n`native_trigger.connection_path`:\n\n| Table | What's Stored |\n|-------|---------------|\n| `workspace_integrations` | `oauth_data` JSON with `base_url`, `client_id`, `client_secret`, `instance_shared` flag. One row per (workspace, service); it must exist for every connection, since token refresh reads the client from it |\n| `variable` | Encrypted `access_token` at the connection path, linked to `account` via its `account` column |\n| `account` | `refresh_token`, with `client` = service name and `is_workspace_integration = true` |\n| `resource` | The connection's resource at the same path, `{ \"token\": \"$var:<path>\" }` |\n\nA user may use a connection when they can read its variable (checked with a user transaction,\nso the variable's row-level security decides): `resolve_usable_connection` /\n`list_usable_connections` in `lib.rs`. The exception is the **workspace connection**: an admin\nmay share one connection per service (`workspace_integrations.resource_path`, set by\n`POST .../workspace_connection`), and every member may then use it without reading its token.\nRenames carry the marker along; disconnecting clears it. Background work (sync, renewal, rename re-registration,\nwebhook handling) uses the stored `connection_path` directly.\n\n`decrypt_oauth_data(db, w_id, service, connection_path)` assembles these into one struct, and\nevery service's `OAuthData` carries `connection_path` so `http_client_request` can refresh and\nwrite back the right connection:\n```rust\npub struct OAuthConfig {\n    pub base_url: String,\n    pub access_token: String,      // decrypted from the connection's variable\n    pub refresh_token: Option<String>, // from its account\n    pub client_id: String,         // from oauth_data or instance settings\n    pub client_secret: String,     // from oauth_data or instance settings\n    pub connection_path: String,\n}\n```\n\nA refreshed token is written back by connection path and account id, never by service: providers\nlike Nextcloud rotate refresh tokens, so writing one onto another account breaks its next refresh.\n\nInstance-level sharing: when the workspace has no OAuth app of its own and the instance admin\nshares one (`share_with_workspaces` in the `oauths` setting; not available for Nextcloud), the\nconnect flow uses it and records `{\"instance_shared\": true}` in `workspace_integrations`.\n\n### URL Resolution\n\nThe `resolve_endpoint()` helper handles both absolute and relative OAuth URLs:\n\n```rust\npub fn resolve_endpoint(base_url: &str, endpoint: &str) -> String {\n    if endpoint.starts_with(\"http://\") || endpoint.starts_with(\"https://\") {\n        endpoint.to_string()  // Google: absolute URLs\n    } else {\n        format!(\"{}{}\", base_url, endpoint)  // Nextcloud: relative paths\n    }\n}\n```\n\n### ServiceName Methods\n\n`ServiceName` is the central registry enum. Each variant must implement these match arms:\n\n| Method | Purpose |\n|--------|---------|\n| `as_str()` | Lowercase identifier (e.g., `\"google\"`) |\n| `as_trigger_kind()` | Maps to `TriggerKind` enum |\n| `as_job_trigger_kind()` | Maps to `JobTriggerKind` enum |\n| `token_endpoint()` | OAuth token endpoint (relative or absolute) |\n| `auth_endpoint()` | OAuth authorization endpoint |\n| `oauth_scopes()` | Space-separated OAuth scopes |\n| `resource_type()` | Resource type for token storage (e.g., `\"gworkspace\"`) |\n| `extra_auth_params()` | Extra OAuth params (e.g., Google needs `access_type=offline`, `prompt=consent`) |\n| `integration_service()` | Maps to the workspace integration service (usually `*self`) |\n| `TryFrom<String>` | Parse from string |\n| `Display` | Delegates to `as_str()` |\n\n---\n\n## Step-by-Step Implementation Guide\n\n### Step 1: Database Migration\n\nCreate a new migration file: `backend/migrations/YYYYMMDDHHMMSS_newservice_trigger.up.sql`\n\n```sql\n-- Add the service to the native_trigger_service enum\nALTER TYPE native_trigger_service ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to TRIGGER_KIND enum (used for trigger tracking)\nALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'newservice';\n\n-- Add to job_trigger_kind enum (used for job tracking)\nALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'newservice';\n```\n\nAlso create the corresponding down migration.\n\n### Step 2: Update windmill-common Enums\n\n#### `backend/windmill-common/src/triggers.rs`\n\nAdd variant to `TriggerKind` enum, and update `to_key()` and `fmt()` implementations.\n\n#### `backend/windmill-common/src/jobs.rs`\n\nAdd variant to `JobTriggerKind` enum and update the `Display` implementation.\n\n### Step 3: Backend Service Module\n\nCreate a new directory: `backend/windmill-native-triggers/src/newservice/`\n\n#### `mod.rs` - Type Definitions\n\n```rust\nuse serde::{Deserialize, Serialize};\n\npub mod external;\n// pub mod routes; // Only if you need additional service-specific routes\n\n/// OAuth data deserialized from the three-table pattern.\n/// The actual structure is built by decrypt_oauth_data() from variable + account + workspace_integrations.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct NewServiceOAuthData {\n    pub base_url: String,              // from workspace_integrations.oauth_data\n    pub access_token: String,          // decrypted from variable table\n    pub refresh_token: Option<String>, // from account table\n    // Note: client_id and client_secret are in OAuthConfig, not here\n    // unless the service needs them at runtime for API calls\n}\n\n/// Configuration provided by user when creating/updating a trigger.\n/// Stored as JSON in native_trigger.service_config.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceConfig {\n    // Service-specific configuration fields\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n}\n\n/// Data retrieved from the external service about a trigger.\n/// Returned by the get() method and shown in the UI.\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct NewServiceTriggerData {\n    pub folder_path: String,\n    pub file_filter: Option<String>,\n    // Fields that shouldn't affect service_config comparison should use #[serde(skip_serializing)]\n}\n\n/// Response from external service when creating a trigger/webhook.\n#[derive(Debug, Deserialize)]\npub struct CreateTriggerResponse {\n    pub id: String,\n}\n\n/// Handler struct (stateless, used for routing)\n#[derive(Copy, Clone)]\npub struct NewService;\n```\n\n#### `external.rs` - External Trait Implementation\n\n```rust\nuse async_trait::async_trait;\nuse reqwest::Method;\nuse sqlx::PgConnection;\nuse std::collections::HashMap;\nuse windmill_common::{\n    error::{Error, Result},\n    BASE_URL, DB,\n};\n\nuse crate::{\n    generate_webhook_service_url, External, NativeTrigger, NativeTriggerData, ServiceName,\n    sync::{SyncError, TriggerSyncInfo},\n};\nuse super::{NewService, NewServiceConfig, NewServiceOAuthData, NewServiceTriggerData, CreateTriggerResponse};\n\n#[async_trait]\nimpl External for NewService {\n    type ServiceConfig = NewServiceConfig;\n    type TriggerData = NewServiceTriggerData;\n    type OAuthData = NewServiceOAuthData;\n    type CreateResponse = CreateTriggerResponse;\n\n    const SERVICE_NAME: ServiceName = ServiceName::NewService;\n    const DISPLAY_NAME: &'static str = \"New Service\";\n    const SUPPORT_WEBHOOK: bool = true;\n    const TOKEN_ENDPOINT: &'static str = \"/oauth/token\";\n    const REFRESH_ENDPOINT: &'static str = \"/oauth/token\";\n    const AUTH_ENDPOINT: &'static str = \"/oauth/authorize\";\n\n    async fn create(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::CreateResponse> {\n        let base_url = &*BASE_URL.read().await;\n\n        // external_id is None during create (we get it from the response)\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            None, Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/create\", oauth_data.base_url);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let response: CreateTriggerResponse = self\n            .http_client_request(&url, Method::POST, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        Ok(response)\n    }\n\n    /// Update returns the resolved service_config as JSON.\n    /// For services using the update+get pattern, call self.get() and serialize.\n    async fn update(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        webhook_token: &str,\n        data: &NativeTriggerData<Self::ServiceConfig>,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<serde_json::Value> {\n        let base_url = &*BASE_URL.read().await;\n\n        let webhook_url = generate_webhook_service_url(\n            base_url, w_id, &data.script_path, data.is_flow,\n            Some(external_id), Self::SERVICE_NAME, webhook_token,\n        );\n\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let payload = serde_json::json!({\n            \"callback_url\": webhook_url,\n            \"folder_path\": data.service_config.folder_path,\n        });\n\n        let _: serde_json::Value = self\n            .http_client_request(&url, Method::PUT, w_id, &oauth_data.connection_path, db, None, Some(&payload))\n            .await?;\n\n        // Fetch back the updated state to get the resolved config\n        let trigger_data = self.get(w_id, oauth_data, external_id, db, tx).await?;\n        serde_json::to_value(&trigger_data)\n            .map_err(|e| Error::InternalErr(format!(\"Failed to serialize trigger data: {}\", e)))\n    }\n\n    async fn get(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<Self::TriggerData> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        self.http_client_request::<_, ()>(&url, Method::GET, w_id, &oauth_data.connection_path, db, None, None).await\n    }\n\n    async fn delete(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<()> {\n        let url = format!(\"{}/api/webhooks/{}\", oauth_data.base_url, external_id);\n        let _: serde_json::Value = self\n            .http_client_request::<_, ()>(&url, Method::DELETE, w_id, &oauth_data.connection_path, db, None, None)\n            .await\n            .or_else(|e| match &e {\n                Error::InternalErr(msg) if msg.contains(\"404\") => Ok(serde_json::Value::Null),\n                _ => Err(e),\n            })?;\n        Ok(())\n    }\n\n    async fn exists(\n        &self,\n        w_id: &str,\n        oauth_data: &Self::OAuthData,\n        external_id: &str,\n        db: &DB,\n        tx: &mut PgConnection,\n    ) -> Result<bool> {\n        match self.get(w_id, oauth_data, external_id, db, tx).await {\n            Ok(_) => Ok(true),\n            Err(Error::NotFound(_)) => Ok(false),\n            Err(e) => Err(e),\n        }\n    }\n\n    /// Background maintenance. Choose the right pattern for your service:\n    /// - For services with queryable external state: use reconcile_with_external_state()\n    /// - For channel-based services with expiration: implement renewal logic\n    async fn maintain_triggers(\n        &self,\n        db: &DB,\n        workspace_id: &str,\n        triggers: &[NativeTrigger],\n        oauth_data: &Self::OAuthData,\n        synced: &mut Vec<TriggerSyncInfo>,\n        errors: &mut Vec<SyncError>,\n    ) {\n        // Option A: Reconcile with external state (Nextcloud pattern)\n        // Fetch all triggers from external service and compare with DB\n        let external_triggers = match self.list_all(workspace_id, oauth_data, db).await {\n            Ok(triggers) => triggers,\n            Err(e) => {\n                errors.push(SyncError {\n                    resource_path: format!(\"workspace:{}\", workspace_id),\n                    error_message: format!(\"Failed to list triggers: {}\", e),\n                    error_type: \"api_error\".to_string(),\n                });\n                return;\n            }\n        };\n\n        // Convert to (external_id, config_json) pairs\n        let external_pairs: Vec<(String, serde_json::Value)> = external_triggers\n            .into_iter()\n            .map(|t| (t.id.clone(), serde_json::to_value(&t).unwrap_or_default()))\n            .collect();\n\n        crate::sync::reconcile_with_external_state(\n            db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n        ).await;\n    }\n\n    fn external_id_and_metadata_from_response(\n        &self,\n        resp: &Self::CreateResponse,\n    ) -> (String, Option<serde_json::Value>) {\n        (resp.id.clone(), None)\n    }\n\n    // service_config_from_create_response: NOT overridden (returns None).\n    // This means the handler uses the update+get pattern after create.\n    // Override and return Some(...) to skip the update+get cycle (Google pattern).\n}\n\nimpl NewService {\n    /// Private helper to list all triggers from the external service.\n    async fn list_all(\n        &self,\n        w_id: &str,\n        oauth_data: &<Self as External>::OAuthData,\n        db: &DB,\n    ) -> Result<Vec<<Self as External>::TriggerData>> {\n        // Implementation depends on the external service's API\n        todo!()\n    }\n}\n```\n\n### Step 4: Update lib.rs Registry\n\nIn `backend/windmill-native-triggers/src/lib.rs`:\n\n```rust\n// Service modules - add new services here:\n#[cfg(feature = \"native_trigger\")]\npub mod newservice;  // <-- Add this\n\n// ServiceName enum - add variant:\npub enum ServiceName {\n    Nextcloud,\n    Google,\n    NewService,  // <-- Add this\n}\n\n// Then add match arms in ALL ServiceName methods:\n// as_str(), as_trigger_kind(), as_job_trigger_kind(), token_endpoint(),\n// auth_endpoint(), oauth_scopes(), resource_type(), extra_auth_params(),\n// integration_service(), TryFrom<String>, Display\n```\n\n### Step 5: Update handler.rs Routes\n\nIn `backend/windmill-native-triggers/src/handler.rs`:\n\n```rust\npub fn generate_native_trigger_routers() -> Router {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n        return router\n            .nest(\"/nextcloud\", service_routes(NextCloud))\n            .nest(\"/google\", service_routes(Google))\n            .nest(\"/newservice\", service_routes(NewService));  // <-- Add this\n    }\n    // ...\n}\n```\n\n### Step 6: Update sync.rs\n\nIn `backend/windmill-native-triggers/src/sync.rs`:\n\n```rust\npub async fn sync_all_triggers(db: &DB) -> Result<BackgroundSyncResult> {\n    // ...\n    #[cfg(feature = \"native_trigger\")]\n    {\n        use crate::newservice::NewService;\n\n        // ... existing service syncs ...\n\n        // New service sync\n        let (service_name, result) = sync_service_triggers(db, NewService).await;\n        total_synced += result.synced_triggers.len();\n        total_errors += result.errors.len();\n        service_results.insert(service_name, result);\n    }\n    // ...\n}\n```\n\n### Step 7: Frontend Service Registry\n\nIn `frontend/src/lib/components/triggers/native/utils.ts`:\n\nAdd to `NATIVE_TRIGGER_SERVICES`, `getTriggerIconName()`, and `getServiceIcon()`.\n\n### Step 8: Frontend Trigger Form Component\n\nCreate: `frontend/src/lib/components/triggers/native/services/newservice/NewServiceTriggerForm.svelte`\n\n### Step 9: Frontend Icon Component\n\nCreate: `frontend/src/lib/components/icons/NewServiceIcon.svelte`\n\n### Step 10: Update NativeTriggerEditor\n\nCheck `frontend/src/lib/components/triggers/native/NativeTriggerEditor.svelte` to ensure it dynamically loads form components based on service name.\n\n### Step 11: Workspace Integration UI\n\nAdd your service to the `supportedServices` map in `frontend/src/lib/components/workspaceSettings/WorkspaceIntegrations.svelte`:\n\n```typescript\nconst supportedServices: Record<string, ServiceConfig> = {\n    // ... existing services ...\n    newservice: {\n        name: 'newservice',\n        displayName: 'New Service',\n        description: 'Connect to New Service for triggers',\n        icon: NewServiceIcon,\n        docsUrl: 'https://www.windmill.dev/docs/integrations/newservice',\n        requiresBaseUrl: false,  // false for cloud services, true for self-hosted\n        setupInstructions: [\n            'Step 1: Create an OAuth app on the service',\n            'Step 2: Configure the redirect URI shown below',\n            'Step 3: Enter the client credentials below'\n        ]\n    }\n}\n```\n\n### Step 12: Update `frontend/src/lib/components/triggers/utils.ts`\n\nUpdate ALL of these maps/functions:\n1. `triggerIconMap` - import and add icon\n2. `triggerDisplayNamesMap` - add display name\n3. `triggerTypeOrder` in `sortTriggers()` - add type\n4. `getLightConfig()` - add case for your service\n5. `getTriggerLabel()` - add case for your service\n6. `jobTriggerKinds` - add to array\n7. `countPropertyMap` - add count property\n8. `triggerSaveFunctions` - add save function\n\n### Step 13: Update TriggersBadge Component\n\nIn `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte`:\n\n1. Import the icon\n2. Add to `baseConfig` with `countKey` (the dynamic `availableNativeServices` loop does NOT set `countKey`)\n3. Add to the `allTypes` array\n\n### Step 14: Update TriggersWrapper.svelte\n\nIn `frontend/src/lib/components/triggers/TriggersWrapper.svelte`:\n\nAdd a `{:else if selectedTrigger.type === 'yourservice'}` case that renders `<NativeTriggersPanel service=\"yourservice\" ...>` with the same props pattern as the existing native trigger cases (e.g., `nextcloud`).\n\n### Step 15: Update AddTriggersButton.svelte\n\nIn `frontend/src/lib/components/triggers/AddTriggersButton.svelte`:\n\n1. Add `yourserviceAvailable` state variable\n2. Add `setYourserviceState()` async function using `isServiceAvailable('yourservice', $workspaceStore!)`\n3. Call it at module level\n4. Add a dropdown entry to `addTriggerItems` with `hidden: !yourserviceAvailable`\n\n### Step 16: Update TriggersEditor.svelte Delete Handling\n\nIn `frontend/src/lib/components/triggers/TriggersEditor.svelte`:\n\nAdd your service to the `nativeTriggerServices` map in `deleteDeployedTrigger()`. Native triggers use `NativeTriggerService.deleteNativeTrigger({ workspace, serviceName, externalId })` instead of the standard `path`-based delete.\n\n### Step 17: Update `getUsedTriggers` for Sidebar Visibility\n\nThe sidebar (`frontend/src/lib/components/sidebar/SidebarContent.svelte`) shows native-trigger links only if `$usedTriggerKinds` includes the service — without this, your trigger page will never appear in the nav bar even when triggers exist.\n\n1. **Backend** — add `{service}_used: bool` to the `UsedTriggers` struct and SELECT in `backend/windmill-api-workspaces/src/workspaces.rs::get_used_triggers()`:\n   ```rust\n   EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = '{service}'::native_trigger_service) AS \"{service}_used!\"\n   ```\n2. **OpenAPI** — add `{service}_used: boolean` to the response schema for `GET /w/{workspace}/workspaces/used_triggers` (under both `properties` and `required`).\n3. **Layout** — in `frontend/src/routes/(root)/(logged)/+layout.svelte::loadUsedTriggerKinds()`, destructure `{service}_used` and push `'{service}'` to `usedKinds`.\n\n### Step 18: Update OpenAPI Spec and Regenerate Types\n\nAdd to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:\n\n```bash\ncd frontend && npm run generate-backend-client\n```\n\n---\n\n## Special Patterns\n\n### Unified Service with `trigger_type` (Google Pattern)\n\nWhen a single service handles multiple trigger types (e.g., Google Drive + Calendar share OAuth and API patterns), use a single `ServiceName` variant with a discriminator field:\n\n```rust\npub enum GoogleTriggerType { Drive, Calendar }\n\npub struct GoogleServiceConfig {\n    pub trigger_type: GoogleTriggerType,\n    // Drive-specific fields (only used when trigger_type = Drive)\n    pub resource_id: Option<String>,\n    pub resource_name: Option<String>,\n    // Calendar-specific fields (only used when trigger_type = Calendar)\n    pub calendar_id: Option<String>,\n    pub calendar_name: Option<String>,\n    // Metadata set after creation\n    pub google_resource_id: Option<String>,\n    pub expiration: Option<String>,\n}\n```\n\nBranch in trait methods based on `trigger_type`. Frontend uses a `ToggleButtonGroup` to switch between types. This keeps the codebase simpler (one service, one OAuth flow, one set of routes).\n\nSee `backend/windmill-native-triggers/src/google/` for the reference implementation.\n\n### Skipping update+get After Create (Google Pattern)\n\nOverride `service_config_from_create_response()` to return `Some(config)` when the external_id is known before the create call:\n\n```rust\nfn service_config_from_create_response(\n    &self,\n    data: &NativeTriggerData<Self::ServiceConfig>,\n    resp: &Self::CreateResponse,\n) -> Option<serde_json::Value> {\n    // Clone input config, add metadata from response\n    let mut config = data.service_config.clone();\n    config.google_resource_id = Some(resp.resource_id.clone());\n    config.expiration = Some(resp.expiration.clone());\n    Some(serde_json::to_value(&config).unwrap())\n}\n```\n\n### Services with Absolute OAuth Endpoints (Google)\n\nUnlike self-hosted services where OAuth endpoints are relative paths appended to `base_url`, services like Google have absolute URLs:\n\n```rust\n// Nextcloud: relative paths\nServiceName::Nextcloud => \"/apps/oauth2/api/v1/token\",\n// Google: absolute URLs\nServiceName::Google => \"https://oauth2.googleapis.com/token\",\n```\n\nThe `resolve_endpoint()` function handles both. For services with absolute endpoints:\n- `base_url` can be empty\n- `requiresBaseUrl: false` in the frontend workspace integration config\n- Add `extra_auth_params()` if needed (Google requires `access_type=offline` and `prompt=consent`)\n\n### Channel-Based Push Notifications with Renewal (Google Pattern)\n\nFor services using expiring watch channels instead of persistent webhooks:\n\n1. Store expiration in `service_config` (as part of `ServiceConfig`)\n2. In `maintain_triggers()`, implement renewal logic instead of using `reconcile_with_external_state()`:\n   ```rust\n   async fn maintain_triggers(&self, db, workspace_id, triggers, oauth_data, synced, errors) {\n       for trigger in triggers {\n           if should_renew_channel(trigger) {\n               self.renew_channel(db, trigger, oauth_data).await;\n           }\n       }\n   }\n   ```\n3. Renewal: best-effort stop old channel, create new one with same external_id, update service_config with new expiration\n4. Google example: Drive channels expire in 24h (renew when <1h left), Calendar channels expire in 7 days (renew when <1 day left)\n\n### reconcile_with_external_state (Nextcloud Pattern)\n\nThe reusable function in `sync.rs` compares external triggers with DB state:\n- Triggers missing externally: sets error \"Trigger no longer exists on external service\"\n- Triggers present externally: clears errors, updates service_config if it differs\n\nUsage in `maintain_triggers()`:\n```rust\nlet external_pairs: Vec<(String, serde_json::Value)> = /* fetch from external */;\ncrate::sync::reconcile_with_external_state(\n    db, workspace_id, Self::SERVICE_NAME, triggers, &external_pairs, synced, errors,\n).await;\n```\n\n### Webhook Payload Processing\n\nOverride `prepare_webhook()` to parse service-specific payloads into script/flow args:\n\n```rust\nasync fn prepare_webhook(&self, db, w_id, headers, body, script_path, is_flow) -> Result<PushArgsOwned> {\n    let mut args = HashMap::new();\n    args.insert(\"event_type\".to_string(), Box::new(headers.get(\"x-event-type\").cloned()) as _);\n    args.insert(\"payload\".to_string(), Box::new(serde_json::from_str::<serde_json::Value>(&body)?) as _);\n    Ok(PushArgsOwned { extra: None, args })\n}\n```\n\nThen register in `prepare_native_trigger_args()` in `lib.rs`:\n```rust\npub async fn prepare_native_trigger_args(service_name, db, w_id, headers, body) -> Result<Option<PushArgsOwned>> {\n    match service_name {\n        ServiceName::Google => { /* ... */ Ok(Some(args)) }\n        ServiceName::NewService => { /* ... */ Ok(Some(args)) }\n        ServiceName::Nextcloud => Ok(None), // Uses default body parsing\n    }\n}\n```\n\n### Instance-Level OAuth Credentials\n\nWhen `workspace_integrations.oauth_data.instance_shared == true`, `decrypt_oauth_data()` reads `client_id` and `client_secret` from instance-level global settings instead of workspace-level. This allows admins to share OAuth app credentials across workspaces.\n\n`generate_connect_url` picks the client (`resolve_oauth_client`): the workspace's own app when configured, the instance's otherwise. A GitHub OAuth app only redirects below its single registered callback, so connections made with the instance GitHub app land on `/oauth/callback/github/native_trigger` instead of the workspace settings page.\n\n---\n\n## Testing Checklist\n\n- [ ] Database migration runs successfully\n- [ ] `cargo check -p windmill-native-triggers --features native_trigger` passes\n- [ ] `npx svelte-check --threshold error` passes (in frontend/)\n- [ ] Service appears in workspace integrations list\n- [ ] OAuth flow completes successfully\n- [ ] Can create a new trigger\n- [ ] Can view trigger details\n- [ ] Can update trigger configuration\n- [ ] Can delete trigger\n- [ ] Webhook receives and processes payloads\n- [ ] Background sync works correctly (reconciliation or channel renewal)\n- [ ] Error handling works (expired tokens, service unavailable)\n\n---\n\n## Reference Implementations\n\n### Nextcloud (Self-Hosted, Update+Get Pattern)\n\n| File | Purpose |\n|------|---------|\n| `nextcloud/mod.rs` | Types: NextCloudOAuthData, NextcloudServiceConfig, NextCloudTriggerData |\n| `nextcloud/external.rs` | External trait: uses update+get pattern, reconcile_with_external_state for sync |\n| `nextcloud/routes.rs` | Additional route: `GET /events` |\n\nKey patterns: relative OAuth endpoints, base_url required, list_all + reconcile for sync, update returns JSON from get().\n\n### Google (Cloud, Unified Service, Short Create)\n\n| File | Purpose |\n|------|---------|\n| `google/mod.rs` | Types: GoogleServiceConfig with trigger_type discriminator, GoogleTriggerType enum |\n| `google/external.rs` | External trait: overrides service_config_from_create_response, channel renewal for sync |\n| `google/routes.rs` | Additional routes: `GET /calendars`, `GET /drive/files`, `GET /drive/shared_drives` |\n\nKey patterns: absolute OAuth endpoints, empty base_url, trigger_type for Drive/Calendar, expiring watch channels with renewal, service_config_from_create_response skips update+get, get() reconstructs data from stored service_config (no external \"get channel\" API).\n\n<!-- File: .agents/skills/pr/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# pr\n\nPath: `.agents/skills/pr/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/pr/SKILL.md)\n\n<!-- File: .agents/skills/refine/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# refine\n\nPath: `.agents/skills/refine/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/refine/SKILL.md)\n\n<!-- File: .agents/skills/rust-backend/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# rust-backend\n\nPath: `.agents/skills/rust-backend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/rust-backend/SKILL.md)\n\n<!-- File: .agents/skills/svelte-frontend/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# svelte-frontend\n\nPath: `.agents/skills/svelte-frontend/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/svelte-frontend/SKILL.md)\n\n<!-- File: .agents/skills/update-sqlx/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# update-sqlx\n\nPath: `.agents/skills/update-sqlx/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/update-sqlx/SKILL.md)\n\n<!-- File: .agents/skills/UPSTREAM.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Skills Skill\n\nPath: `.agents/skills/UPSTREAM.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.agents/skills/UPSTREAM.md)\n\n<!-- File: .claude/skills/adding-a-trigger/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# adding-a-trigger\n\nPath: `.claude/skills/adding-a-trigger/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/adding-a-trigger/SKILL.md)\n\n<!-- File: .claude/skills/ai-chat/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# ai-chat\n\nPath: `.claude/skills/ai-chat/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-chat/SKILL.md)\n\n<!-- File: .claude/skills/ai-evals/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# ai-evals\n\nPath: `.claude/skills/ai-evals/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/ai-evals/SKILL.md)\n\n<!-- File: .claude/skills/codebase-design/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# codebase-design\n\nPath: `.claude/skills/codebase-design/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/windmill-labs/windmill/HEAD/.claude/skills/codebase-design/SKILL.md)\n\n<!-- ... +56 more modular skills available via /api/skills/windmill-labs/windmill?path={path} -->\n</yakaai_skills>"}