home / skills / windmill-labs / windmill / write-script-rust

This skill guides you in writing Rust scripts with a serializable return type and proper main signature for Windmill automation.

npx playbooks add skill windmill-labs/windmill --skill write-script-rust

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
1.9 KB
---
name: write-script-rust
description: MUST use when writing Rust scripts.
---

## CLI Commands

Place scripts in a folder. After writing, run:
- `wmill script generate-metadata` - Generate .script.yaml and .lock files
- `wmill sync push` - Deploy to Windmill

Use `wmill resource-type list --schema` to discover available resource types.

# Rust

## Structure

The script must contain a function called `main` with proper return type:

```rust
use anyhow::anyhow;
use serde::Serialize;

#[derive(Serialize, Debug)]
struct ReturnType {
    result: String,
    count: i32,
}

fn main(param1: String, param2: i32) -> anyhow::Result<ReturnType> {
    Ok(ReturnType {
        result: param1,
        count: param2,
    })
}
```

**Important:**
- Arguments should be owned types
- Return type must be serializable (`#[derive(Serialize)]`)
- Return type is `anyhow::Result<T>`

## Dependencies

Packages must be specified with a partial cargo.toml at the beginning of the script:

```rust
//! ```cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! reqwest = { version = "0.11", features = ["json"] }
//! tokio = { version = "1", features = ["full"] }
//! ```

use anyhow::anyhow;
// ... rest of the code
```

**Note:** Serde is already included, no need to add it again.

## Async Functions

If you need to handle async functions (e.g., using tokio), keep the main function sync and create the runtime inside:

```rust
//! ```cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! tokio = { version = "1", features = ["full"] }
//! reqwest = { version = "0.11", features = ["json"] }
//! ```

use anyhow::anyhow;
use serde::Serialize;

#[derive(Serialize, Debug)]
struct Response {
    data: String,
}

fn main(url: String) -> anyhow::Result<Response> {
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(async {
        let resp = reqwest::get(&url).await?.text().await?;
        Ok(Response { data: resp })
    })
}
```

Overview

This skill provides a clear, opinionated template and checklist for writing Rust scripts that run on the platform. It enforces the required function signature, serialization rules, dependency embedding, and patterns for mixing sync and async code. Use it to produce scripts that deploy cleanly and interoperate with the runtime and resource tooling.

How this skill works

It inspects your script for a main function with owned argument types and a return of anyhow::Result<T> where T implements serde::Serialize. It requires a partial Cargo manifest embedded as a code fence at the top of the file so the runtime knows which crates to compile. For async work, it recommends creating a tokio runtime inside a synchronous main so the script stays compatible with the platform.

When to use it

  • When authoring any Rust script intended for deployment to the platform
  • When returning structured data that must be serialized for downstream steps or UI
  • When your code needs external crates—declare them in the embedded cargo block
  • When you need to call async APIs from a script but keep the main function sync
  • When preparing scripts for automated metadata generation and deployment

Best practices

  • Declare arguments as owned types (e.g., String, i32) to avoid lifetime issues
  • Return anyhow::Result<T> where T derives serde::Serialize for consistent serialization
  • Place a partial cargo.toml block at the top using the provided code-fence format
  • Keep the top-level main function synchronous; spawn a tokio runtime for async calls
  • Avoid re-declaring serde in dependencies; it is already provided by the platform

Example use cases

  • Return structured results from a data-transform script that populates a UI table
  • Call an external REST API in an async block and return the response as a serializable struct
  • Write a script that queries a PostgreSQL resource and returns aggregated counts
  • Create small automation hooks that accept input parameters, run logic, and return JSON-ready results
  • Bundle third-party crates by listing them in the embedded cargo section before the code

FAQ

What must the main function look like?

main must accept owned arguments and return anyhow::Result<T>, where T derives serde::Serialize.

How do I use async code in a script?

Keep main synchronous and create a tokio runtime inside it, then block_on your async work.

Where do I declare dependencies?

Embed a partial [dependencies] Cargo manifest at the top of the script inside the specified code-fence format; serde is already included.