refactor: slim down claude instructions for lean context and fast iteration (#8136)

* refactor: slim down claude instructions for lean context and fast iteration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add private and license feature flags to enterprise validation docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add /refine skill for end-of-session doc evolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: remove architecture.md overview doc per research findings

General codebase overviews distract agents and trigger unnecessary
exploration. Keep only operational docs (validation, enterprise).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add autonomous mode doc for bypass permission workflows

Covers: plan-first requirement, tmux pane usage for checking
backend/frontend logs, manual testing via Playwright MCP,
Playwright gotchas, and end-of-task summary expectations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add mermaid, playwright, and asciinema tools to autonomous mode doc

Claude should use mmdc for diagrams during planning, playwright CLI for
screenshots of frontend changes, and asciinema for terminal recordings
of CLI changes. All attached to the PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use pastebin for screenshot/recording uploads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review findings

- Remove stale docs/architecture.md reference from /refine skill
- Fix script name: ./update-sqlx -> ./update_sqlx.sh
- Remove .claude/settings.local.json mention from enterprise doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-28 07:00:19 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 33036baca2
commit bd5239fb7e
9 changed files with 366 additions and 1056 deletions
+39
View File
@@ -0,0 +1,39 @@
---
name: refine
user_invocable: true
description: End-of-session reflection. Reviews friction encountered during the session and proposes updates to docs/ to capture lessons learned.
---
# Refine Skill
Reflect on the current session and update documentation with lessons learned.
## Instructions
1. **Identify friction**: Review what happened in this session:
- Run `git diff main...HEAD --stat` to see what files were touched
- Think about: what was slow, what failed, what required multiple attempts, what information was missing or hard to find
2. **Read current docs**: Read the docs that were relevant to this session:
- `docs/validation.md`
- `docs/enterprise.md`
- `docs/autonomous-mode.md`
- Any skills that were invoked
3. **Propose updates**: For each piece of friction, decide if it warrants a doc update:
- **Missing knowledge**: Information you had to discover that should be documented
- **Wrong guidance**: Instructions that led you astray
- **Missing validation rule**: A check that should be in the validation matrix
- **New pattern**: A codebase pattern worth capturing for next time
4. **Apply updates**: Edit the relevant `docs/` files. Keep changes minimal and specific — add only what would have saved time this session.
5. **Report**: Summarize what was added/changed and why.
## Rules
- Only add knowledge confirmed by this session — no speculative additions
- Keep docs concise — add a line or two, not a paragraph
- If a whole new doc is needed, create it in `docs/` and add a pointer in `CLAUDE.md`
- Don't update skills unless a coding pattern was genuinely wrong
- Don't add things Claude already knows — only Windmill-specific knowledge
+49 -437
View File
@@ -3,493 +3,105 @@ name: rust-backend
description: Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory.
---
# Rust Backend Coding Guidelines
# Windmill Rust Patterns
Apply these patterns when writing or modifying Rust code in the `backend/` directory.
## Data Structure Design
Choose between `struct`, `enum`, or `newtype` based on domain needs:
- Use `enum` for state machines instead of boolean flags or loosely related fields
- Model invariants explicitly using types (e.g., `NonZeroU32`, `Duration`, custom enums)
- Consider ownership of each field:
- Use `&str` vs `String`, slices vs vectors
- Use `Arc<T>` when sharing across threads
- Use `Cow<'a, T>` for flexible ownership
```rust
// State machine with enum
enum JobState {
Pending { scheduled_for: DateTime<Utc> },
Running { started_at: DateTime<Utc>, worker: String },
Completed { result: JobResult, duration_ms: i64 },
Failed { error: String, retries: u32 },
}
// Avoid multiple booleans
struct Job {
is_pending: bool, // Don't do this
is_running: bool,
is_completed: bool,
}
```
## Impl Block Organization
Place `impl` blocks immediately below the struct/enum they modify. Group methods logically:
```rust
struct JobQueue {
jobs: Vec<Job>,
capacity: usize,
}
impl JobQueue {
// Constructors first
pub fn new(capacity: usize) -> Self { ... }
pub fn with_jobs(jobs: Vec<Job>) -> Self { ... }
// Getters
pub fn len(&self) -> usize { ... }
pub fn is_empty(&self) -> bool { ... }
// Mutation methods
pub fn push(&mut self, job: Job) -> Result<()> { ... }
pub fn pop(&mut self) -> Option<Job> { ... }
// Domain logic
pub fn next_scheduled(&self) -> Option<&Job> { ... }
}
```
## Iterator Chains Over For-Loops
Prefer functional iterator chains (`.filter().map().collect()`) over imperative for-loops:
```rust
// Preferred
let results: Vec<_> = items
.iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
// Avoid
let mut results = Vec::new();
for item in items.iter() {
if item.is_valid() {
results.push(item.transform());
}
}
```
Apply these Windmill-specific patterns when writing Rust code in `backend/`.
## Error Handling
Use the `Error` type from `windmill_common::error`. Return `Result<T, Error>` or `JsonResult<T>` for fallible functions:
Use `Error` from `windmill_common::error`. Return `Result<T, Error>` or `JsonResult<T>`:
```rust
use windmill_common::error::{Error, Result};
// Use ? operator for propagation
pub async fn get_job(db: &DB, id: Uuid) -> Result<Job> {
let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id)
sqlx::query_as!(Job, "SELECT id, workspace_id FROM v2_job WHERE id = $1", id)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::NotFound("job not found".to_string()))?;
Ok(job)
}
```
Prefer `if let` for optional handling. Use `let...else` when early return makes code clearer:
Never panic in library code. Reserve `.unwrap()` for compile-time guarantees.
## SQLx Patterns
**Never use `SELECT *`** — always list columns explicitly. Critical for backwards compatibility when workers lag behind API version:
```rust
let Some(config) = get_config() else {
return Err(Error::MissingConfig);
};
// Correct
sqlx::query_as!(Job, "SELECT id, workspace_id, path FROM v2_job WHERE id = $1", id)
// Wrong — breaks when columns are added
sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", id)
```
Never panic in library code. Reserve `.unwrap()` for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity.
## Early Returns
Return early to avoid deep nesting. Handle error cases and edge conditions first:
Use batch operations to avoid N+1:
```rust
// Preferred - early returns
fn process_job(job: Option<Job>) -> Result<Output> {
let Some(job) = job else {
return Ok(Output::default());
};
if !job.is_valid() {
return Err(Error::InvalidJob);
}
if job.is_cached() {
return Ok(job.cached_result());
}
// Main logic at the end, not nested
execute_job(job)
}
// Avoid - deep nesting
fn process_job(job: Option<Job>) -> Result<Output> {
if let Some(job) = job {
if job.is_valid() {
if !job.is_cached() {
execute_job(job)
} else {
Ok(job.cached_result())
}
} else {
Err(Error::InvalidJob)
}
} else {
Ok(Output::default())
}
}
// Preferred — single query with IN clause
sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?
```
## Variable Shadowing
Shadow variables instead of creating new names with prefixes:
```rust
// Preferred
let data = fetch_raw_data();
let data = parse(data);
let data = validate(data)?;
// Avoid
let raw_data = fetch_raw_data();
let parsed_data = parse(raw_data);
let validated_data = validate(parsed_data)?;
```
## Minimal Comments
- No inline comments explaining obvious code
- No TODO/FIXME comments in committed code
- Doc comments (`///`) only on public items
- Let code be self-documenting through clear naming
## Type Safety
Use enums over boolean flags for clarity:
```rust
// Preferred
enum JobStatus {
Pending,
Running,
Completed,
}
// Avoid
struct Job {
is_running: bool,
is_completed: bool,
}
```
## Pattern Matching
Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields:
```rust
// Explicit matching preferred
match status {
JobStatus::Pending => handle_pending(),
JobStatus::Running => handle_running(),
JobStatus::Completed => handle_completed(),
}
// Wildcards OK for fallback
match result {
Ok(value) => process(value),
Err(_) => return default_value(),
}
// Wildcards OK for ignoring fields in destructuring
let Point { x, y, .. } = point;
```
## Destructuring in Function Signatures
Destructure structs directly in function parameters:
```rust
// Preferred
async fn process_job(
Extension(db): Extension<DB>,
Path((workspace, job_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Job>> {
// ...
}
// Avoid
async fn process_job(
db_ext: Extension<DB>,
path: Path<(String, Uuid)>,
query: Query<Pagination>,
) -> Result<Json<Job>> {
let Extension(db) = db_ext;
let Path((workspace, job_id)) = path;
// ...
}
```
## Trait Implementations
Use standard trait implementations to simplify conversions and reduce boilerplate:
```rust
// Implement From/Into for type conversions
impl From<DbJob> for ApiJob {
fn from(db: DbJob) -> Self {
ApiJob {
id: db.id,
status: db.status.into(),
}
}
}
// Use TryFrom for fallible conversions
impl TryFrom<String> for JobKind {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> { ... }
}
```
Apply `derive` macros to reduce boilerplate:
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job { ... }
```
## Module Structure
- Use `pub(crate)` instead of `pub` when possible; expose only what needs exposing
- Keep APIs small and expressive; avoid leaking internal types
- Organize code into modules reflecting ownership and domain boundaries
```rust
// Prefer restricted visibility
pub(crate) fn internal_helper() { ... }
// Only pub for external API
pub fn create_job(...) -> Result<Job> { ... }
```
## Code Navigation
Always use rust-analyzer LSP for:
- Go to definition
- Find references
- Type information
- Import resolution
Do not guess at module paths or type definitions.
Use transactions for multi-step operations. Parameterize all queries.
## JSON Handling
Prefer `Box<serde_json::value::RawValue>` over `serde_json::Value` when:
- Storing JSON in the database (JSONB columns)
- Passing JSON through without modification
- The JSON structure doesn't need inspection
Prefer `Box<serde_json::value::RawValue>` over `serde_json::Value` when storing/passing JSON without inspection:
```rust
// Preferred - avoids parsing/serialization overhead
pub struct Job {
pub id: Uuid,
pub args: Option<Box<serde_json::value::RawValue>>,
}
// Only use Value when you need to inspect/modify JSON
let value: serde_json::Value = serde_json::from_str(&json)?;
if let Some(field) = value.get("field") {
// modify or inspect
}
```
## Serde Optimizations
Only use `serde_json::Value` when you need to inspect or modify the JSON.
Use serde attributes to optimize serialization:
## Serde Optimizations
```rust
#[derive(Serialize, Deserialize)]
pub struct Job {
#[serde(rename = "jobId")]
pub id: Uuid,
#[serde(default)]
pub priority: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default)]
pub priority: i32,
}
```
Prefer borrowing for zero-copy deserialization when lifetimes allow:
## Async & Concurrency
Never block the async runtime. Use `spawn_blocking` for CPU-intensive work:
```rust
#[derive(Deserialize)]
pub struct JobInput<'a> {
#[serde(borrow)]
pub workspace_id: Cow<'a, str>,
#[serde(borrow)]
pub script_path: &'a str,
}
let result = tokio::task::spawn_blocking(move || expensive_computation(&data)).await?;
```
## SQLx Patterns
**Mutex selection**: Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) for data protection. Only use `tokio::sync::Mutex` when holding locks across `.await` points.
**Never use `SELECT *`** - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version:
Use `tokio::sync::mpsc` (bounded) for channels. Avoid `std::thread::sleep` in async contexts.
## Module Structure & Visibility
- Use `pub(crate)` instead of `pub` when possible
- Place new code in the appropriate crate based on functionality
- API endpoints go in `windmill-api/src/` organized by domain
- Shared functionality goes in `windmill-common/src/`
## Code Navigation
Always use rust-analyzer LSP for go-to-definition, find-references, and type info. Do not guess at module paths.
## Axum Handlers
Destructure extractors directly in function signatures:
```rust
// Preferred - explicit columns
sqlx::query_as!(
Job,
"SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1",
job_id
)
// Avoid - breaks when columns are added
sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id)
async fn process_job(
Extension(db): Extension<DB>,
Path((workspace, job_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Job>> { ... }
```
Use batch operations to minimize round trips:
```rust
// Preferred - single query with multiple values
sqlx::query!(
"INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)",
id1, log1, id2, log2
)
// Avoid N+1 queries
for id in ids {
sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?;
}
// Preferred - single query with IN clause
sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?
```
Use transactions for multi-step operations and parameterize all queries.
## Async & Tokio Patterns
Never block the async runtime. Use `spawn_blocking` for CPU-intensive or blocking I/O:
```rust
// Preferred - offload blocking work
let result = tokio::task::spawn_blocking(move || {
expensive_computation(&data)
}).await?;
// Avoid - blocks the runtime
let result = expensive_computation(&data); // Don't do this in async
```
Use tokio primitives for sleep and channels:
```rust
use tokio::sync::mpsc;
use tokio::time::sleep;
// Avoid in async contexts
use std::thread::sleep; // Blocks the runtime
```
Use bounded channels for backpressure:
```rust
// Preferred - bounded channel prevents overwhelming
let (tx, rx) = tokio::sync::mpsc::channel(100);
// Be careful with unbounded
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
```
## Mutex Selection in Async Code
**Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) over `tokio::sync::Mutex`** for protecting data in async code. The async mutex is more expensive and only needed when holding locks across `.await` points.
```rust
// Preferred for data protection - std mutex is faster
use std::sync::Mutex;
struct Cache {
data: Mutex<HashMap<String, Value>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<Value> {
self.data.lock().unwrap().get(key).cloned()
}
fn insert(&self, key: String, value: Value) {
self.data.lock().unwrap().insert(key, value);
}
}
```
**Use `tokio::sync::Mutex` only when you must hold the lock across `.await` points**, typically for IO resources like database connections:
```rust
use tokio::sync::Mutex;
use std::sync::Arc;
// Async mutex for IO resources held across await points
let conn = Arc::new(Mutex::new(db_connection));
async fn execute_query(conn: Arc<Mutex<DbConn>>, query: &str) {
let mut lock = conn.lock().await;
lock.execute(query).await; // Lock held across .await
}
```
**Common pattern**: Wrap `Arc<Mutex<...>>` in a struct with non-async methods that lock internally, keeping lock scope minimal:
```rust
struct SharedState {
inner: std::sync::Mutex<StateInner>,
}
impl SharedState {
fn update(&self, value: i32) {
self.inner.lock().unwrap().value = value;
}
fn get(&self) -> i32 {
self.inner.lock().unwrap().value
}
}
```
**Alternative for IO resources**: Spawn a dedicated task to manage the resource and communicate via message passing:
```rust
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
handle_io_command(&mut resource, cmd).await;
}
});
```
## Build & Tooling
Build speed tips:
- Use `cargo check` during rapid iteration over `cargo build`
- Minimize unnecessary dependencies and feature flags
+43 -281
View File
@@ -3,316 +3,78 @@ name: svelte-frontend
description: Svelte coding guidelines for the Windmill frontend. MUST use when writing or modifying code in the frontend directory.
---
# Svelte 5 Best Practices
# Windmill Svelte Patterns
This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so.
Apply these Windmill-specific patterns when writing Svelte code in `frontend/`. For general Svelte 5 syntax (runes, snippets, event handling), use the Svelte MCP server.
## Reactivity with Runes
## Windmill UI Components (MUST use)
Svelte 5 introduces Runes for more explicit and flexible reactivity.
Always use Windmill's design-system components. Never use raw HTML elements.
1. **Embrace Runes for State Management**:
* Use `$state` for reactive local component state.
```svelte
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
```
* Use `$derived` for computed values based on other reactive state.
```svelte
<script>
let count = $state(0);
const doubled = $derived(count * 2);
</script>
<p>{count} * 2 = {doubled}</p>
```
* Use `$effect` for side effects that need to run when reactive values change (e.g., logging, manual DOM manipulation, data fetching). Remember `$effect` does not run on the server.
```svelte
<script>
let count = $state(0);
$effect(() => {
console.log('The count is now', count);
if (count > 5) {
alert('Count is too high!');
}
});
</script>
```
2. **Props with `$props`**:
* Declare component props using `$props()`. This offers better clarity and flexibility compared to `export let`.
```svelte
<script>
// ChildComponent.svelte
let { name, age = $state(30) } = $props();
</script>
<p>Name: {name}</p>
<p>Age: {age}</p>
```
* For bindable props, use `$bindable`.
```svelte
<script>
// MyInput.svelte
let { value = $bindable() } = $props();
</script>
<input bind:value />
```
## Event Handling
* **Use direct event attributes**: Svelte 5 moves away from `on:` directives for DOM events.
* **Do**: `<button onclick={handleClick}>...</button>`
* **Don't**: `<button on:click={handleClick}>...</button>`
* **For component events, prefer callback props**: Instead of `createEventDispatcher`, pass functions as props.
```svelte
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let message = $state('');
function handleChildEvent(detail) {
message = detail;
}
</script>
<Child onCustomEvent={handleChildEvent} />
<p>Message from child: {message}</p>
<!-- Child.svelte -->
<script>
let { onCustomEvent } = $props();
function emitEvent() {
onCustomEvent('Hello from child!');
}
</script>
<button onclick={emitEvent}>Send Event</button>
```
## Snippets for Content Projection
* **Use `{#snippet ...}` and `{@render ...}` instead of slots**: Snippets are more powerful and flexible.
```svelte
<!-- Parent.svelte -->
<script>
import Card from './Card.svelte';
</script>
<Card>
{#snippet title()}
My Awesome Title
{/snippet}
{#snippet content()}
<p>Some interesting content here.</p>
{/snippet}
</Card>
<!-- Card.svelte -->
<script>
let { title, content } = $props();
</script>
<article>
<header>{@render title()}</header>
<div>{@render content()}</div>
</article>
```
* Default content is passed via the `children` prop (which is a snippet).
```svelte
<!-- Wrapper.svelte -->
<script>
let { children } = $props();
</script>
<div>
{@render children?.()}
</div>
```
## Component Design
1. **Create Small, Reusable Components**: Break down complex UIs into smaller, focused components. Each component should have a single responsibility. This also aids performance by limiting the scope of reactivity updates.
2. **Descriptive Naming**: Use clear and descriptive names for variables, functions, and components.
3. **Minimize Logic in Components**: Move complex business logic to utility functions or services. Keep components focused on presentation and interaction.
## State Management (Stores)
1. **Segment Stores**: Avoid a single global store. Create multiple stores, each responsible for a specific piece of global state (e.g., `userStore.js`, `themeStore.js`). This can help limit reactivity updates to only the parts of the UI that depend on specific state segments.
2. **Use Custom Stores for Complex Logic**: For stores with related methods, create custom stores.
```javascript
// counterStore.js
import { writable } from 'svelte/store';
function createCounter() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const counter = createCounter();
```
3. **Use Context API for Localized State**: For state shared within a component subtree, consider Svelte's context API (`setContext`, `getContext`) instead of global stores when the state doesn't need to be truly global.
## Performance Optimizations (Svelte 5)
When generating Svelte 5 code, prioritize frontend performance by applying the following principles:
### General Svelte 5 Principles
- **Leverage the Compiler:** Trust Svelte's compiler to generate optimized JavaScript. Avoid manual DOM manipulation (`document.querySelector`, etc.) unless absolutely necessary for integrating third-party libraries that lack Svelte adapters.
- **Keep Components Small and Focused:** Reinforcing from Component Design, smaller components lead to less complex reactivity graphs and more targeted, efficient updates.
### Reactivity & State Management
- **Optimize Computations with `$derived`:** Always use `$derived` for computed values that depend on other state. This ensures the computation only runs when its specific dependencies change, avoiding unnecessary work compared to recomputing derived values in `$effect` or less efficient methods.
- **Minimize `$effect` Usage:** Use `$effect` sparingly and only for true side effects that interact with the outside world or non-Svelte state. Avoid putting complex logic or state updates *within* an `$effect` unless those updates are explicitly intended as a reaction to external changes or non-Svelte state. Excessive or complex effects can impact rendering performance.
- **Structure State for Fine-Grained Updates:** Design your `$state` objects or variables such that updates affect only the necessary parts of the UI. Avoid putting too much unrelated state into a single large object that gets frequently updated, as this can potentially trigger broader updates than necessary. Consider normalizing complex, nested state.
### List Rendering (`{#each}`)
- **Mandate `key` Attribute:** Always use a `key` attribute (`{#each items as item (item.id)}`) that refers to a unique, stable identifier for each item in a list. This is critical for allowing Svelte to efficiently update, reorder, add, or remove list items without destroying and re-creating unnecessary DOM elements and component instances.
### Component Loading & Bundling
- **Implement Lazy Loading/Code Splitting:** For routes, components, or modules that are not immediately needed on page load, use dynamic imports (`import(...)`) to split the code bundle. SvelteKit handles this automatically for routes, but it can be applied manually to components using helper patterns if needed.
- **Be Mindful of Third-Party Libraries:** When incorporating external libraries, import only the necessary functions or components to minimize the final bundle size. Prefer libraries designed to be tree-shakeable.
### Rendering & DOM
- **Use CSS for Animations/Transitions:** Prefer CSS animations or transitions where possible for performance. Svelte's built-in `transition:` directive is also highly optimized and should be used for complex state-driven transitions, but simple cases can often use plain CSS.
- **Optimize Image Loading:** Implement best practices for images: use optimized formats (WebP, AVIF), lazy loading (`loading="lazy"`), and responsive images (`<picture>`, `srcset`) to avoid loading unnecessarily large images.
### Server-Side Rendering (SSR) & Hydration
- **Ensure SSR Compatibility:** Write components that can be rendered on the server for faster initial page loads. Avoid relying on browser-specific APIs (like `window` or `document`) in the main `<script>` context. If necessary, use `$effect` or check `if (browser)` inside effects to run browser-specific code only on the client.
- **Minimize Work During Hydration:** Structure components and data fetching such that minimal complex setup or computation is required when the client-side Svelte code takes over from the server-rendered HTML. Heavy synchronous work during hydration can block the main thread.
## General Clean Code Practices
1. **Organized File Structure**: Group related files together. A common structure:
```
/src
|-- /routes // Page components (if using a router like SvelteKit)
|-- /lib // Utility functions, services, constants (SvelteKit often uses this)
| |-- /stores
| |-- /utils
| |-- /services
| |-- /components // Reusable UI components
|-- App.svelte
|-- main.js (or main.ts)
```
2. **Scoped Styles**: Keep CSS scoped to components to avoid unintended side effects and improve maintainability. Avoid `:global` where possible.
3. **Immutability**: With Svelte 5 and `$state`, direct assignments to properties of `$state` objects (`obj.prop = value;`) are generally fine as Svelte's reactivity system handles updates. However, for non-rune state or when interacting with other systems, understanding and sometimes preferring immutable updates (creating new objects/arrays) can still be relevant.
4. **Use `class:` and `style:` directives**: For dynamic classes and styles, use Svelte's built-in directives for cleaner templates and potentially optimized updates.
```svelte
<script>
let isActive = $state(true);
let color = $state('blue');
</script>
<div class:active={isActive} style:color={color}>
Hello
</div>
```
5. **Stay Updated**: Keep Svelte and its related packages up to date to benefit from the latest features, performance improvements, and security fixes.
## Windmill UI Component Rules (MUST follow)
Always use Windmill's own design-system components instead of raw HTML elements. Using raw HTML elements produces inconsistent styling and breaks the design language.
### Icons — use `lucide-svelte`
**Never** write inline SVGs. Import icons from `lucide-svelte`.
```svelte
<script>
import { ChevronLeft, ChevronRight, X } from 'lucide-svelte'
</script>
<ChevronLeft size={16} />
```
### Buttons — use `<Button>`
**Never** use `<button>`. Import and use `Button` from `$lib/components/common`.
### Buttons — `<Button>`
```svelte
<script>
import { Button } from '$lib/components/common'
import { ChevronLeft, ChevronRight } from 'lucide-svelte'
import { ChevronLeft } from 'lucide-svelte'
</script>
<!-- Regular button -->
<Button variant="default" onclick={handleClick}>Label</Button>
<!-- Icon-only button (no label) -->
<Button startIcon={{ icon: ChevronLeft }} iconOnly onclick={prevMonth} />
<Button startIcon={{ icon: ChevronRight }} iconOnly onclick={nextMonth} />
<Button startIcon={{ icon: ChevronLeft }} iconOnly onclick={prev} />
```
Key `Button` props:
- `variant?: 'accent' | 'accent-secondary' | 'default' | 'subtle'`
- `unifiedSize?: 'sm' | 'md' | 'lg'`
- `startIcon?: { icon: SvelteComponent }` — renders an icon before the label
- `iconOnly?: boolean` — renders icon with no surrounding label text
- `disabled?: boolean`
Props: `variant?: 'accent' | 'accent-secondary' | 'default' | 'subtle'`, `unifiedSize?: 'sm' | 'md' | 'lg'`, `startIcon?: { icon: SvelteComponent }`, `iconOnly?: boolean`, `disabled?: boolean`
### Text inputs — use `<TextInput>`
**Never** use `<input>`. Import and use `TextInput` from `$lib/components/common`.
### Text inputs — `<TextInput>`
```svelte
<script>
import { TextInput } from '$lib/components/common'
let val = $state('')
</script>
<TextInput bind:value={val} placeholder="Enter value" />
```
Key `TextInput` props:
- `value?: string | number` (bindable)
- `placeholder?: string`
- `disabled?: boolean`
- `error?: string | boolean`
- `size?: 'sm' | 'md' | 'lg'`
- `inputProps?` — forwarded to the underlying `<input>`
Props: `value?: string | number` (bindable), `placeholder?: string`, `disabled?: boolean`, `error?: string | boolean`, `size?: 'sm' | 'md' | 'lg'`
### Selects — use `<Select>`
**Never** use `<select>`. Import and use `Select` from `$lib/components/select/Select.svelte`.
### Selects — `<Select>`
```svelte
<script>
import Select from '$lib/components/select/Select.svelte'
const monthItems = [
{ label: 'January', value: 1 },
{ label: 'February', value: 2 },
// ...
]
let selectedMonth = $state(1)
</script>
<Select items={monthItems} bind:value={selectedMonth} />
<Select items={[{ label: 'Jan', value: 1 }]} bind:value={selected} />
```
Key `Select` props:
- `items?: Array<{ label?: string; value: any; subtitle?: string; disabled?: boolean }>`
- `value` (bindable) — the currently selected `.value`
- `placeholder?: string`
- `clearable?: boolean`
- `disabled?: boolean`
- `size?: 'sm' | 'md' | 'lg'`
Props: `items?: Array<{ label?: string; value: any }>`, `value` (bindable), `placeholder?: string`, `clearable?: boolean`, `size?: 'sm' | 'md' | 'lg'`
### Icons — `lucide-svelte`
Never write inline SVGs. Import from `lucide-svelte`:
```svelte
<script>
import { ChevronLeft, X } from 'lucide-svelte'
</script>
<ChevronLeft size={16} />
```
## Form Components
Form components (TextInput, Toggle, Select, etc.) should use the unified size system when placed together.
## Styling
- Use Tailwind CSS for all styling — no custom CSS
- Use Windmill's theming classes for colors/surfaces (see `frontend/brand-guidelines.md`)
- Read component props JSDoc before using them
## Svelte MCP Server
Use the Svelte MCP tools when working on Svelte code:
1. **list-sections**: Call first to discover available docs
2. **get-documentation**: Fetch relevant sections based on use_cases
3. **svelte-autofixer**: MUST use on all Svelte code before finalizing — keep calling until no issues
4. **playground-link**: Only after user confirms and code was NOT written to project files
+23 -57
View File
@@ -1,68 +1,34 @@
# Windmill Development Guide
# Windmill
## Overview
Open-source platform for internal tools, workflows, API integrations, background jobs, and UIs. Rust backend + Svelte 5 frontend.
Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details.
## Workflow
## New Feature Implementation Guidelines
1. **Understand**: Before coding, read relevant docs from `docs/` to understand the area you're changing
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
When implementing new features in Windmill, follow these best practices:
## Documentation
- **Clean Code First**: Write clean, readable, and maintainable code. Prioritize clarity over cleverness.
- **Avoid Duplication at All Costs**: Before writing new code, thoroughly search for existing implementations that can be reused or extended.
- **Adapt Existing Code**: Refactor and generalize existing code when necessary to avoid logic duplication. Extract common patterns into reusable utilities.
- **Follow Established Patterns**: Study existing code patterns in the codebase and maintain consistency with established conventions.
- **Single Responsibility**: Each function, component, and module should have a single, well-defined responsibility.
- **Incremental Implementation**: Break large features into smaller, reviewable chunks that can be implemented and tested incrementally.
## Language-Specific Guides
- Backend (Rust): see `backend/CLAUDE.md` and the `rust-backend` skill: `.claude/skills/rust-backend/SKILL.md`
- Frontend (Svelte 5): see `frontend/CLAUDE.md` and the `svelte-frontend` skill: `.claude/skills/svelte-frontend/SKILL.md`
- **Validation**: `docs/validation.md` — what checks to run based on what you changed
- **Autonomous mode**: `docs/autonomous-mode.md` — when running in bypass/auto permission mode
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
- **Brand/UI guidelines**: `frontend/brand-guidelines.md`
## Dev Environment
- **Backend**: `cargo run` from `backend/` (API at http://localhost:8000)
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/`
- The `REMOTE` env var configures the Vite proxy target. Without it, API calls proxy to `https://app.windmill.dev` instead of the local backend.
- The dev server starts on port 3000 (or 3001+ if 3000 is in use).
- **Default login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings` (opens the drawer overlay)
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/` (port 3000+)
- **DB**: `psql postgres://postgres:changeme@localhost:5432/windmill`
- **Login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings`
## UI Testing with Playwright MCP
## Core Principles
When testing the frontend with the Playwright MCP tools:
1. **Start servers**: Launch backend (`cargo run`) and frontend (`REMOTE=http://localhost:8000 npm run dev`) as background tasks
2. **Wait for readiness**: Backend takes ~60s to compile; check output for `health check completed`. Frontend starts in ~5s.
3. **Login flow**: Navigate to `/user/login`, click "Log in without third-party", fill email/password, submit
4. **Instance settings drawer**: Navigate to `/#superadmin-settings` to open the drawer directly
5. **Toggle components**: The YAML toggle uses a custom `<Toggle>` component where the checkbox is visually hidden (`sr-only`). Click the wrapper `<label>` element (the parent container with `cursor=pointer`), not the checkbox ref directly.
6. **Console errors to ignore**: `critical_alerts` 404s are expected on CE builds (EE-only endpoint). VSCode worker 404s are dev-mode artifacts.
## Code Validation (MUST DO)
After making code changes, you MUST run the appropriate checks and fix all errors before considering the work done:
- **Backend**: Run `cargo check` from the `backend/` directory. Only enable the feature flags needed for the code you changed — check `backend/Cargo.toml` `[features]` section to identify which flags gate the crates/modules you modified. For example: `cargo check --features enterprise,parquet` if you only touched enterprise and parquet code.
- **Frontend**: Run `npm run check` from the `frontend/` directory.
## Querying the Database
`backend/summarized_schema.txt` provides a compact overview of all tables, columns, types, ENUMs, and foreign keys. Use it to quickly understand the data model and relationships. Note: this file is a simplified summary — it omits indexes, constraints details, and other metadata.
For exact table definitions (indexes, constraints, column defaults, etc.), query the database directly:
```bash
psql postgres://postgres:changeme@localhost:5432/windmill
```
Useful psql commands:
- `\d <table_name>` — full table definition with indexes and constraints
- `\di <table_name>*` — list indexes for a table
- `\d+ <table_name>` — extended table info including storage and descriptions
This is also helpful for:
- Inspecting database state during development
- Testing queries before implementing them in Rust
- Debugging data-related issues
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked
+7 -97
View File
@@ -1,98 +1,8 @@
# Backend Development (Rust)
# Backend (Rust)
## Project Structure
Windmill uses a workspace-based architecture with multiple crates:
- **windmill-api**: API server functionality
- **windmill-worker**: Job execution
- **windmill-common**: Shared code used by all crates
- **windmill-queue**: Job & flow queuing
- **windmill-audit**: Audit logging
- Other specialized crates (git-sync, autoscaling, etc.)
## Key References (MUST FOLLOW THESE)
- You MUST follow best-practices by using the `rust-backend` skill, everytime you write RUST code.
- When working with the database: read `summarized_schema.txt` before starting
- When working with the API routes: you can read `windmill-api/src/lib.rs` to get started
## Adding New Code
### Module Organization
- Place new code in the appropriate crate based on functionality
- For API endpoints, create or modify files in `windmill-api/src/` organized by domain
- For shared functionality, use `windmill-common/src/`
- Follow existing patterns for file structure and organization
### API Endpoints
- Follow existing patterns in the `windmill-api` crate
- Use axum's routing system and extractors
- Update `backend/windmill-api/openapi.yaml` after modifying API endpoints
### Database Changes
- Update database schema with migration if necessary
- Use `sqlx` for database operations with prepared statements
- Use transactions for multi-step operations
- To apply pending migrations: `sqlx migrate run` (never manually run .sql files)
- **Never use `SQLX_OFFLINE=true`** — a live database is always available for compilation
- After all code changes are done, run `./update-sqlx` to regenerate the offline query cache
## Enterprise Features
- Enterprise files use the `*_ee.rs` suffix
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private` or `~/windmill-ee-private`), symlinked into each crate's `src/`
- The `_ee.rs` files are gitignored in the main repo — they are tracked only in the `windmill-ee-private` repo
- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there)
- Use feature flags: `#[cfg(feature = "enterprise")]`
- Isolate enterprise code in separate modules
### EE PR Workflow (MUST DO when modifying `*_ee.rs` files)
When you modify any `*_ee.rs` file and create a PR on the windmill repo, you **MUST** also:
1. **Create a matching branch** in the `windmill-ee-private` repo (use the same branch name). If using worktrees, the EE worktree is at `~/windmill-ee-private__worktrees/<branch-name>/`
2. **Commit and push** the `_ee.rs` changes in that branch
3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR
4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` to write the latest EE commit hash. **Important**: the script may fall back to `~/windmill-ee-private` (main branch) instead of the worktree — verify it wrote the correct commit hash from your branch, not from main. If wrong, manually write the correct hash.
5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
## Code Validation (MUST DO)
After making backend changes, you MUST run `cargo check` and fix all errors and warnings before considering the work done.
Only enable the feature flags relevant to your changes — do NOT use `all_sqlx_features` as it compiles the entire codebase and is very slow. Check the `[features]` section in `Cargo.toml` to identify which flags gate the crates/modules you modified.
Examples:
```bash
# Changed core code (no feature-gated modules)
cargo check
# Changed code behind the enterprise feature
cargo check --features enterprise
# Changed kafka trigger code
cargo check --features kafka
```
## Git Workflow
- **Never push directly to main** — always create a branch and open a pull request
## Testing
- Write unit tests for core functionality
- Use the `#[cfg(test)]` module for test code
- For database tests, use the existing test utilities
## Common Crates
- **tokio**: Async runtime
- **axum**: Web server and routing
- **sqlx**: Database operations
- **serde**: Serialization/deserialization
- **tracing**: Logging and diagnostics
- **reqwest**: HTTP client
- **Coding patterns**: MUST use the `rust-backend` skill when writing Rust code
- **Validation**: `docs/validation.md` — which `cargo check` flags to use
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **DB schema**: `backend/summarized_schema.txt`
- **API routes entry point**: `windmill-api/src/lib.rs`
- **OpenAPI spec**: `windmill-api/openapi.yaml`
+94
View File
@@ -0,0 +1,94 @@
# Autonomous Mode (Bypass Permissions)
When running in bypass/auto permission mode, follow these instructions to work end-to-end without human intervention.
## Available Tools
The Nix devShell provides these tools for documentation and testing:
- **`mmdc`** (mermaid-cli): Generate diagrams from Mermaid markup. Uses Nix-provided headless Chrome via `$PUPPETEER_EXECUTABLE_PATH`.
- **`asciinema`**: Record terminal sessions as `.cast` files for demo videos.
- **`playwright`** CLI: Take screenshots of the running frontend.
### When to Use Them
- **Designing a feature**: Use `mmdc` to generate Mermaid diagrams (architecture, data flow, sequence diagrams) during the planning phase. Include them in the PR description.
- **Frontend changes**: Take screenshots with the Playwright CLI after manual testing. Attach them to the PR.
- **CLI / terminal changes**: Record a demo with `asciinema` showing the feature in action. Attach to the PR.
### Quick Reference
```bash
# Generate a diagram
echo 'graph LR; A-->B; B-->C;' | mmdc -i - -o diagram.png
# Take a screenshot of a page
playwright screenshot --browser chromium http://localhost:3000 screenshot.png
# Record a terminal demo
asciinema rec demo.cast
# ... do the demo ...
# ctrl-d to stop
```
## Always Plan First
Even in bypass mode, **enter plan mode before starting non-trivial work**. Ask all important questions upfront:
- Clarify ambiguous requirements before writing code
- Identify which files, crates, and features are affected
- Read `docs/validation.md` to know what checks you'll need to run
- Break large features into stages — commit each stage separately
## Dev Environment (tmux)
Backend and frontend are running in tmux panes in the current session:
- **Pane 0**: This pane (Claude agent)
- **Pane 1**: Backend (`cargo watch -x run`)
- **Pane 2**: Frontend (`npm run dev`)
### Checking Logs
```bash
# Backend logs (last 50 lines)
tmux capture-pane -t .1 -p -S -50
# Frontend logs (last 50 lines)
tmux capture-pane -t .2 -p -S -50
```
Backend runs with `cargo watch`, so Rust changes auto-recompile — just check pane 1 logs for errors. No need to run `cargo check` separately during iteration if the backend pane shows a successful build.
## Manual Testing
After code changes compile and type-check, verify the feature works:
1. **Check backend logs** (`tmux capture-pane -t .1 -p -S -50`) — confirm no panics or errors
2. **Check frontend logs** (`tmux capture-pane -t .2 -p -S -50`) — confirm no build errors
3. **Use Playwright MCP** to test the UI flow:
- Navigate to `http://localhost:3000/user/login`
- Click "Log in without third-party"
- Login with `admin@windmill.dev` / `changeme`
- Navigate to the page affected by your change
- Verify the feature works as expected
4. **Test edge cases**: empty states, error states, permissions
### Playwright Gotchas
- Backend takes ~60s to compile on first change; check logs for `health check completed`
- Frontend rebuilds in ~5s
- `critical_alerts` 404s are expected on CE builds (EE-only endpoint) — ignore them
- VSCode worker 404s are dev-mode artifacts — ignore them
- The `<Toggle>` component hides the checkbox (`sr-only`). Click the `<label>` wrapper, not the checkbox
## End-of-Task Summary
When done, provide:
- What was changed and why (files modified, approach taken)
- What checks passed (cargo check, npm run check, etc.)
- What was manually tested and the results
- **Screenshots** of UI changes (via `playwright screenshot`)
- **Terminal recordings** of CLI changes (via asciinema)
- Any known limitations or follow-up work needed
Upload images via pastebin (e.g., `curl -F 'file=@screenshot.png' https://0x0.st`) and include the URLs in the PR description or comments.
+38
View File
@@ -0,0 +1,38 @@
# Enterprise (EE) Development
## File Conventions
- Enterprise files use the `*_ee.rs` suffix
- Source lives in `windmill-ee-private` (sibling repo), symlinked into each crate's `src/`
- `_ee.rs` files are gitignored in the main windmill repo — tracked only in `windmill-ee-private`
- Use feature flags: `#[cfg(feature = "enterprise")]` for enterprise logic
- The `private` feature flag gates compilation of `*_ee.rs` files
- The `license` feature flag gates features that require a valid license key at runtime
- Isolate enterprise code in separate modules
## Finding the EE Repo
- Standard location: `~/windmill-ee-private`
- Worktree location: `~/windmill-ee-private__worktrees/<branch-name>/`
## EE PR Workflow (MUST DO when modifying `*_ee.rs` files)
When you modify any `*_ee.rs` file and create a PR on windmill:
1. **Create a matching branch** in `windmill-ee-private` (same branch name)
2. **Commit and push** the `_ee.rs` changes in that branch
3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR
4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/`
- **Verify** it wrote the correct commit hash from your branch, not from main (the script may fall back to `~/windmill-ee-private` on main)
- If wrong, manually write the correct hash
5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
## Validation
```bash
# EE code (always include private to compile *_ee.rs files)
cargo check --features enterprise,private
# EE code that also requires license validation
cargo check --features enterprise,private,license
```
+55
View File
@@ -0,0 +1,55 @@
# Validation Check Matrix
After making changes, run the appropriate checks and fix all errors before considering work done.
## Backend: What to Check
| What changed | Command | Notes |
|---|---|---|
| Core code (no feature gates) | `cargo check` | |
| Enterprise code (`*_ee.rs`) | `cargo check --features enterprise,private` | Also do EE PR workflow (see `docs/enterprise.md`) |
| Enterprise + license-gated code | `cargo check --features enterprise,private,license` | When the feature requires a valid license key |
| Kafka trigger code | `cargo check --features kafka` | |
| Native trigger code | `cargo check --features native_trigger` | |
| Parquet code | `cargo check --features parquet` | |
| Multiple gated modules | `cargo check --features enterprise,parquet` | Combine only the flags you need |
| API route changes | `cargo check` | Then update `openapi.yaml` and run `npm run generate-backend-client` |
| Database migrations | `cargo check` | Test migration applies cleanly with `sqlx migrate run` |
**Never** use `--features all_sqlx_features` — it compiles everything and is very slow. Check `backend/Cargo.toml` `[features]` to find the right flags.
**Never** use `SQLX_OFFLINE=true` — a live database is always available.
After all code changes are done, run `./update_sqlx.sh` from `backend/` to regenerate the offline query cache.
## Frontend: What to Check
| When | Command | Time |
|---|---|---|
| During iteration | `npm run check:fast` | ~2s |
| Final PR validation | `npm run check` | ~50s |
| After backend API changes | `npm run generate-backend-client` first | |
## Cross-Cutting Checks
| Situation | Extra step |
|---|---|
| Added/modified API endpoints | Update `backend/windmill-api/openapi.yaml`, regenerate client |
| Modified Flow structures | Also update `openflow.openapi.yaml` |
| Changed DB schema | Update `backend/summarized_schema.txt` if needed |
| Enterprise file changes | Companion PR in `windmill-ee-private` (see `docs/enterprise.md`) |
## When to Write Tests
- **New utility functions** in `windmill-common`: always add unit tests
- **New API endpoints** with complex logic: add integration test
- **Bug fixes** for non-obvious bugs: add regression test
- **Pure UI changes**: no tests required (rely on type checking)
- **Refactoring**: ensure existing tests pass, don't add new ones
## When to Check Performance
Run `EXPLAIN ANALYZE` on new/modified queries when touching:
- Job queue tables (`v2_job`, `v2_job_completed`)
- Hot-path queries (polling, scheduling)
- Added/removed indexes
+18 -184
View File
@@ -1,195 +1,29 @@
# Frontend Development (Svelte 5)
# Frontend (Svelte 5)
## Core Principles
- **Coding patterns**: MUST use the `svelte-frontend` skill when writing Svelte code
- **Validation**: `docs/validation.md``npm run check:fast` (2s) for iteration, `npm run check` (50s) for final PR
- **UI components**: use Windmill's design-system components (Button, TextInput, Select) — never raw HTML elements
- **Brand/design**: `frontend/brand-guidelines.md`
- **Backend API**: routes in `../backend/windmill-api/openapi.yaml`, generated types in `src/lib/gen/`
- **Regenerate client**: `npm run generate-backend-client` after backend API changes
- Follow the `svelte-frontend` skill for best practices: .claude/skills/svelte-frontend/SKILL.md
- Use Runes ($state, $derived, $effect) for reactivity
- Keep components small and focused
- Always use keys in {#each} blocks
## Key Frontend Patterns
## Data Flow and State Management
### Prefer Unidirectional Data Flow with Composable State
When you can use unidirectional data flow with composable state, prefer that over two-way binding between components. Two-way binding between components creates confusing data flow - as the codebase grows, nothing guarantees that bound state won't be updated from multiple locations, leading to bugs and maintenance issues.
**❌ AVOID: Two-way binding when composable state would work**
```svelte
<Loader bind:loading bind:items {args} />
```
**✅ PREFER: Unidirectional data flow with composables**
### Prefer Composable State Over Two-Way Binding
```typescript
// loader.svelte.ts
// Use resource() from runed for async data
import { resource } from 'runed'
let items = resource(() => args, (args) => SomeService.list(args))
// items.loading, items.current
// Use composables for shared reactive state
function useLoader(argsGetter: () => Args) {
let args = $derived(argsGetter())
let items = $state([])
let loading = $state(false)
$effect(() => {
// Logic reactive to args changes
})
return {
get loading() { return loading },
get items() { return items }
}
$effect(() => { /* react to argsGetter() */ })
return { get loading() { return loading }, get items() { return items } }
}
// Component.svelte
<script>
let loader = useLoader(() => args)
let loading = $derived(loader.loading)
let items = $derived(loader.items)
</script>
```
This pattern ensures:
- State responsibility is clearly owned by `useLoader`
- Data flows in one direction (parent → child)
- No ambiguity about where state can be modified
- Better maintainability as the codebase scales
### Async Data Fetching with Runed
For async requests, **always use `resource()` from the Runed library** instead of manual state management:
```typescript
import { resource } from 'runed'
let items = resource(() => args, (args) => YourService.route(args))
// Access loading state
items.loading
// Access data
items.current
```
The `resource()` utility:
- Automatically handles loading states
- Manages async lifecycle
- Provides reactive updates when dependencies change
- Eliminates boilerplate for common async patterns
**Key Takeaway**: Prefer unidirectional data flow with composables over two-way binding between components. Two-way binding is acceptable for simple form inputs, but avoid it when composable state patterns can provide clearer state ownership.
## UI Guidelines
### Styling Guidelines
- **Use Tailwind CSS** for all styling instead of custom CSS
- **Use Windmill's theming classes** for consistent colors and surfaces
- **Avoid custom styles** - prefer Tailwind utility classes
- **Follow existing patterns** - look at other components for reference
- **Respect design guidelines** - rules are defined in 'brand-guidelines.md'
### UI Components
- Use frontend/src/lib/components/common/button/Button.svelte for all buttons
- Use the component TextInput for all text inputs
- Form components (TextInputs, ToggleButtons, Select ...) should all use the same size when put together, using the unified size system.
- Read carefully components props JSDoc before using them
## Code Validation (MUST DO)
After making frontend changes, you MUST run the following and fix all errors and warnings before considering the work done:
```bash
npm run check:fast
```
At the end of a PR to do final validation, you can do the longer one (2s for fast vs 50s for the slow one):
```bash
npm run check
```
## Backend API
- If you need to call the backend API, you can find the available routes in ../backend/windmill-api/openapi.yaml
- You can also use the associated types and services that are auto generated from the openapi file. They are in src/lib/gen/\*gen.ts files
### OpenAPI Autogeneration
Windmill automatically generates TypeScript types and services from the OpenAPI specification.
#### Service Generation Pattern
The autogeneration follows this pattern:
- **Tag** → **Service Name**: The OpenAPI tag becomes the service name with "Service" suffix
- **operationId** → **Method Name**: The operationId becomes the method name in the service
#### Example
Given this OpenAPI specification:
```yaml
/w/{workspace}/audit/list:
get:
summary: list audit logs (requires admin privilege)
operationId: listAuditLogs
tags:
- audit
parameters:
- $ref: '#/components/parameters/WorkspaceId'
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
- $ref: '#/components/parameters/Before'
- $ref: '#/components/parameters/After'
- $ref: '#/components/parameters/Username'
- $ref: '#/components/parameters/Operation'
- name: operations
in: query
description: comma separated list of exact operations to include
schema:
type: string
```
This generates:
- **Service**: `AuditService` (from tag "audit")
- **Method**: `listAuditLogs` (from operationId)
#### Method Arguments
The generated method arguments correspond to the OpenAPI parameters:
```typescript
AuditService.listAuditLogs({
workspace: string, // from WorkspaceId parameter
page?: number, // from Page parameter
perPage?: number, // from PerPage parameter
before?: string, // from Before parameter
after?: string, // from After parameter
username?: string, // from Username parameter
operation?: string, // from Operation parameter
operations?: string // from operations parameter
})
```
## Svelte 5 documentation
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
Two-way binding is fine for simple form inputs. Avoid it for component-to-component state.