Skip to main content

write-script-python3

MUST use when writing Python scripts.

소스 정보

저장소
windmill-labs/windmill
최근 소스 활동
2026년 10월 3일 07:33
감지된 SKILL.md 언어
영어
스타
18,107
포크
1,111

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
write-script-python3
description
MUST use when writing Python scripts.
## CLI Commands Place scripts in a folder. After writing, tell the user which command fits what they want to do: - `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying. - `wmill script run <path>` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits. - `wmill generate-metadata` — regenerate the local `.script.yaml` (input schema) and `.lock` (resolved dependencies) for scripts you changed, and refresh their content hashes in `wmill-lock.yaml`. Local files only — **not** a deploy. See "Keep metadata in sync" below. - Deploy local changes to the workspace — via `git push` or `wmill sync push` depending on how the repo is wired (see the **Deploying** section in `AGENTS.wmill.md`). Only suggest/run a deploy when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test". ### Preview vs run — choose by intent, not habit If the user says "run the script", "try it", "test it", "does it work" while there are **local edits to the script file**, use `script preview`. Do NOT push the script to then `script run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes. Only use `script run` when: - The user explicitly says "run the deployed version" / "run what's on the server". - There is no local script being edited (you're just invoking an existing script). Only use `sync push` when: - The user explicitly asks to deploy, publish, push, or ship. - The preview has already validated the change and the user wants it in the workspace. ### Keep metadata in sync after editing `wmill-lock.yaml` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing `main`'s arguments** — invalidates that hash and leaves the `.lock`, the `.script.yaml` input schema, and the hash row out of date. Run `wmill generate-metadata` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by `.script.yaml`), and `wmill-lock.yaml` all match the code. Leaving them stale produces spurious diffs in git-sync and CI. This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's `AGENTS.md` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated `.lock` / `.script.lock` files and tell the user which dependency versions changed (e.g. `requests 2.31.0 → 2.32.0`), so they can catch an unwanted bump before deploying — even under `Metadata: auto`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed. With no path argument, `generate-metadata` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run `wmill generate-metadata --dry-run` — it lists each stale item with a reason (`content changed` or `depends on <path>`) without changing anything — then narrow with a path argument (`wmill generate-metadata f/foo`) or `--strict-folder-boundaries`. If the on-disk `.lock` and `.script.yaml` are already correct and only `wmill-lock.yaml` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use `wmill generate-metadata rehash` — it re-records hashes from disk with no backend round-trip and no dependency changes. ### After writing — offer to test, don't wait passively If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run `wmill script preview` with sample args?"). Do not present a multi-option menu. If the user already asked to test/run/try the script in their original request, skip the offer and just execute `wmill script preview <path> -d '<args>'` directly — pick plausible args from the script's declared parameters. The shape varies by language: `main(...)` for code languages, the SQL dialect's own placeholder syntax (`$1` for PostgreSQL, `?` for MySQL/Snowflake, `@P1` for MSSQL, `@name` for BigQuery, etc.), positional `$1`, `$2`, … for Bash, `param(...)` for PowerShell. `wmill script preview` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). `wmill generate-metadata` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's `AGENTS.md` opts in), per "Keep metadata in sync" above. Deploying to the workspace (`git push` or `wmill sync push` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push. For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill. Use `wmill resource-type list --schema` to discover available resource types. # Windmill Script Writing Guide ## General Principles - A script's inputs are its parameters. Credentials and configuration come in as resource-typed parameters, never hard-coded or read from the environment; the language section below shows how that language declares parameters - Libraries are installed automatically - do not show installation instructions - In a language with an entrypoint function (TypeScript, Python, Go, Rust, PHP, R, …), name it `main` (`Main` in C#) and do not call it; in TypeScript it must be async. SQL, GraphQL, Bash, PowerShell and Ansible scripts have no `main`: their language section shows how they take arguments - Where the language has a Windmill client (`wmill`), use it to interact with the platform - A script's input schema may carry a top-level `prompt_for_ai` string: its author's instructions to an AI choosing the inputs. Follow it when you pick arguments to run that script, and keep it when you rewrite the schema ## Return Values - A script can return any JSON-serializable value; a SQL script returns the rows its query produces - Return values become available to subsequent flow steps via `results.step_id` ## Preprocessor Scripts Preprocessor scripts process raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, AMQP, Postgres, GCP Pub/Sub, Azure, or email) before passing it to the flow. This separates the trigger logic from the flow logic and keeps the auto-generated UI clean. A preprocessor is written in TypeScript or Python: its function is named `preprocessor` instead of `main`, and it receives a single parameter called `event` (the language section gives its type). The returned object determines the parameter values passed to the flow. e.g., `{ b: 1, a: 2 }` calls the flow with `a = 2` and `b = 1`, assuming the flow has two inputs called `a` and `b`. # Python ## Structure The script must contain at least one function called `main`: ```python def main(param1: str, param2: int): # Your code here return {"result": param1, "count": param2} ``` Do not call the main function. Libraries are installed automatically. ## Resource Types On Windmill, credentials and configuration are stored in resources and passed as parameters to main. You need to **redefine** the type of the resources that are needed before the main function as TypedDict: ```python from typing import TypedDict class postgresql(TypedDict): host: str port: int user: str password: str dbname: str def main(db: postgresql): # db contains the database connection details pass ``` **Important rules:** - The resource type name must be **IN LOWERCASE** - Only include resource types if they are actually needed - If an import conflicts with a resource type name, **rename the imported object, not the type name** - Make sure to import TypedDict from typing **if you're using it** ## Imports Libraries are installed automatically. Do not show installation instructions. ```python import requests import pandas as pd from datetime import datetime ``` If an import name conflicts with a resource type: ```python # Wrong - don't rename the type import stripe as stripe_lib class stripe_type(TypedDict): ... # Correct - rename the import import stripe as stripe_sdk class stripe(TypedDict): api_key: str ``` ## Windmill Client Import the windmill client for platform interactions: ```python import wmill ``` See the SDK documentation for available methods. ## Preprocessor Scripts For preprocessor scripts, the function should be named `preprocessor` and receives an `event` parameter: ```python from typing import TypedDict, Literal, Any class Event(TypedDict): kind: Literal["webhook", "http", "websocket", "kafka", "email", "nats", "postgres", "sqs", "mqtt", "amqp", "gcp", "azure"] body: Any headers: dict[str, str] query: dict[str, str] def preprocessor(event: Event): # Transform the event into flow input parameters return { "param1": event["body"]["field1"], "param2": event["query"]["id"] } ``` ## S3 Object Operations Windmill provides built-in support for S3-compatible storage operations. ### Receiving an S3Object as a script parameter To accept a file from S3 as input to a script, type the parameter with `S3Object` (imported from `wmill`): ```python import wmill from wmill import S3Object def main(file: S3Object): content = wmill.load_s3_file(file) # ... ``` ### S3 operations ```python import wmill # Load file content from S3 content: bytes = wmill.load_s3_file(s3object) # Load file as stream reader reader: BufferedReader = wmill.load_s3_file_reader(s3object) # Write file to S3 result: S3Object = wmill.write_s3_file( s3object, # Target path (or None to auto-generate) file_content, # bytes or BufferedReader s3_resource_path, # Optional: specific S3 resource content_type, # Optional: MIME type content_disposition # Optional: Content-Disposition header ) ``` # Python SDK (wmill) Import: import wmill The client configures itself from the job's environment — base URL, token and credentials mode are all set before your code runs, so there is nothing to initialize and no reason to read WM_TOKEN or BASE_INTERNAL_URL and build an API URL yourself. Reconstructing that by hand only reintroduces details the client already handles. Call the SDK for anything Windmill, and use raw HTTP for third-party APIs. The functions below are the surface to prefer. For an endpoint none of them covers, wmill.Windmill().get(endpoint) and .post(endpoint) issue an authenticated request against this instance. What does not exist is a function name you guessed at: if it is not listed below, do not call it. To know who is running the script, read the contextual variables rather than calling the API: `os.environ.get("WM_END_USER_EMAIL") or os.environ.get("WM_EMAIL")`. WM_END_USER_EMAIL is the app viewer when the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL is the user the job is permissioned as. WM_USERNAME is the matching username. def worker_has_internal_server() -> bool def get_mocked_api() -> Optional[dict] # Get the HTTP client instance. # # Returns: # Configured httpx.Client for API requests def get_client() -> httpx.Client # Make an HTTP GET request to the Windmill API. # # Args: # endpoint: API endpoint path # raise_for_status: Whether to raise an exception on HTTP errors # **kwargs: Additional arguments passed to httpx.get # # Returns: # HTTP response object def get(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # Make an HTTP POST request to the Windmill API. # # Args: # endpoint: API endpoint path # raise_for_status: Whether to raise an exception on HTTP errors # **kwargs: Additional arguments passed to httpx.post # # Returns: # HTTP response object def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response # Create a new authentication token. # # Args: # duration: Token validity duration (default: 1 day) # # Returns: # New authentication token string def create_token(duration = dt.timedelta(days=1)) -> str # Create a script job by path and return its job id. def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str # Create a script job by hash and return its job id. def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str # Create a flow job and return its job id. def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str # Run script by path synchronously and return its result. def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any # Run script by hash synchronously and return its result. def run_script_by_hash(hash_: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any # Run a script on the current worker without creating a job. # # On agent workers (no internal server), falls back to running a normal # preview job and waiting for the result. def run_inline_script_preview(content: str, language: str, args: dict = None) -> Any # Wait for a job to complete and return its result. # # Args: # job_id: ID of the job to wait for # timeout: Maximum time to wait (seconds or timedelta) # verbose: Enable verbose logging # cleanup: Register cleanup handler to cancel job on exit # assert_result_is_not_none: Raise exception if result is None # # Returns: # Job result when completed # # Raises: # TimeoutError: If timeout is reached # Exception: If job fails def wait_job(job_id, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False) # Cancel a specific job by ID. # # Args: # job_id: UUID of the job to cancel # reason: Optional reason for cancellation # # Returns: # Response message from the cancel endpoint def cancel_job(job_id: str, reason: str = None) -> str # Cancel currently running executions of the same script. def cancel_running() -> dict # Get job details by ID. # # Args: # job_id: UUID of the job # # Returns: # Job details dictionary def get_job(job_id: str) -> dict # Get the root job ID for a flow hierarchy. # # Args: # job_id: Job ID (defaults to current WM_JOB_ID) # # Returns: # Root job ID def get_root_job_id(job_id: str | None = None) -> dict # Get an OIDC JWT token for authentication to external services. # # Args: # audience: Token audience (e.g., "vault", "aws") # expires_in: Optional expiration time in seconds # # Returns: # JWT token string def get_id_token(audience: str, expires_in: int | None = None) -> str # Get the status of a job. # # Args: # job_id: UUID of the job # # Returns: # Job status: "RUNNING", "WAITING", or "COMPLETED" def get_job_status(job_id: str) -> JobStatus # Get the result of a completed job. # # Args: # job_id: UUID of the completed job # assert_result_is_not_none: Raise exception if result is None # # Returns: # Job result def get_result(job_id: str, assert_result_is_not_none: bool = True) -> Any # Get a variable value by path. # # Args: # path: Variable path in Windmill # # Returns: # Variable value as string def get_variable(path: str) -> str # Set a variable value by path, creating it if it doesn't exist. # # Args: # path: Variable path in Windmill # value: Variable value to set # is_secret: Whether the variable should be secret (default: False) def set_variable(path: str, value: str, is_secret: bool = False) -> None # Get a resource value by path. # # Args: # path: Resource path in Windmill # none_if_undefined: Return None instead of raising if not found # interpolated: if variables and resources are fully unrolled # # Returns: # Resource value dictionary or None def get_resource(path: str, none_if_undefined: bool = False, interpolated: bool = True) -> dict | None # Set a resource value by path, creating it if it doesn't exist. # # Args: # value: Resource value to set # path: Resource path in Windmill # resource_type: Resource type for creation def set_resource(value: Any, path: str, resource_type: str) # List resources from Windmill workspace. # # Args: # resource_type: Optional resource type to filter by (e.g., "postgresql", "mysql", "s3") # page: Optional page number for pagination # per_page: Optional number of results per page # # Returns: # List of resource dictionaries
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기