Turn scripts into internal tools with web forms
To turn a script into an internal tool, write it as a function in Windmill: Windmill reads the function's parameters and generates a web form from them, so a colleague can fill it in and run the script without a terminal. The same script also gets an API endpoint, a run history with logs, and can be placed in an app when one form is not enough. This works for Python, TypeScript and the other supported languages, on Windmill Cloud or a self-hosted instance.
1. Write the script as a function
Here is a script a support team could use to refund an order.
Every parameter of main becomes a field of the form:
from typing import Literal
def main(
order_id: str,
amount_eur: float,
reason: Literal["damaged", "late", "duplicate", "other"],
notify_customer: bool = True,
note: str = "",
):
# Call your billing API here. The return value is shown to the user.
return {
"order_id": order_id,
"refunded_eur": round(amount_eur, 2),
"reason": reason,
"customer_notified": notify_customer,
"note": note,
}
Save it in the script editor, or locally with the CLI as f/ops/refund_order.py.
2. The form Windmill generates
Windmill parses the parameters into a JSON schema and renders the form from it. For the script above, the schema has:
| Parameter | Field in the form |
|---|---|
order_id: str | Required text field |
amount_eur: float | Required number field |
reason: Literal[...] | Required dropdown with the four values |
notify_customer: bool = True | Toggle, on by default |
note: str = "" | Optional text field |
Type hints set the field type, and default values make a field optional. In Generated UI you can add descriptions, placeholders, enums and string formats such as email or date-time without changing the code. For a dropdown whose options come from a database or an API, use a dynamic select.
Secrets do not belong in the form. Store API keys as variables or resources, and read them in the script.
3. Let people run it
Members with the Operator role run and view what others built without authoring scripts, flows or apps. Put the script in a folder shared with the team that uses it. They open the script, fill in the form, run it and see the result.
Every run is recorded with its inputs, logs, result and the user who started it, in the runs page.
4. Call the same script from other tools
The script also has webhook endpoints. The synchronous one waits for the result:
curl -X POST "https://<your-instance>/api/w/<workspace>/jobs/run_wait_result/p/f/ops/refund_order" \
-H "Authorization: Bearer $WINDMILL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"order_id": "A-1042", "amount_eur": 19.999, "reason": "damaged"}'
{"note": "", "reason": "damaged", "order_id": "A-1042", "refunded_eur": 20.0, "customer_notified": true}
Fields left out of the request take their default values. Use a webhook-specific token when another system calls it, so the token can run this script and nothing else. The same script can also run on a schedule or as a step of a flow.
5. When one form is not enough
- Several steps, or a step that waits for a manager: chain scripts in a flow, which also gets a form from its inputs, and add an approval step.
- A page with tables, several actions and your own layout: build a full-code app in React or Svelte that calls your scripts as backend runnables, or use the low-code app editor.
- People outside the workspace: apps can be shared through a secret URL as public apps.
Edition notes
The form, the webhook endpoints, the Operator role and the run history are part of the open-source Community Edition. On a self-hosted Community Edition, SSO is limited to 10 users, the runs history is kept for up to 30 days, and audit logs are redacted. Redirecting operators to a workspace default app when they log in is a Cloud and Enterprise Edition feature.