An ops engineer told me: 'We had 200 internal scripts scattered across cron jobs, Lambda functions, and random VMs. Nobody knew which ones were still running or who owned them.' Windmill solved this — all scripts in one place with a UI, scheduling, and approvals.
What Windmill Offers for Free
Windmill self-hosted (free forever):
- Unlimited users, scripts, and executions
- Script editor — TypeScript, Python, Go, Bash, SQL, GraphQL
- Workflow builder — visual DAG editor
- App builder — drag-and-drop internal tools
- Scheduling — cron with retry and error handling
- Approval flows — human-in-the-loop workflows
- REST API — manage everything programmatically
- Auto-generated UIs — scripts become forms automatically
Windmill Cloud free tier: 1,000 executions/month.
Quick Start
# Self-hosted via Docker
git clone https://github.com/windmill-labs/windmill.git
cd windmill/docker
docker compose up -d
# Access at http://localhost:8000
Write a Script (Auto-Gets a UI)
# main.py — This becomes an internal tool automatically!
# Windmill generates a form UI from the type annotations
def main(
customer_email: str,
discount_percent: int = 10,
reason: str = "loyalty",
notify_slack: bool = True
):
"""Apply a discount to a customer's next invoice."""
# Apply discount in Stripe
import stripe
stripe.api_key = os.environ["STRIPE_API_KEY"]
customer = stripe.Customer.list(email=customer_email).data[0]
coupon = stripe.Coupon.create(percent_off=discount_percent, duration="once")
stripe.Customer.modify(customer.id, coupon=coupon.id)
if notify_slack:
# Post to Slack
requests.post(os.environ["SLACK_WEBHOOK"], json={
"text": f"Applied {discount_percent}% discount to {customer_email} ({reason})"
})
return {"customer": customer_email, "discount": f"{discount_percent}%", "status": "applied"}
REST API
# Run a script
curl -X POST 'http://localhost:8000/api/w/workspace/jobs/run/p/f/folder/script_name' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"customer_email": "alice@example.com", "discount_percent": 15}'
# Get job result
curl 'http://localhost:8000/api/w/workspace/jobs/completed/get/JOB_ID' \
-H 'Authorization: Bearer YOUR_TOKEN'
# List scripts
curl 'http://localhost:8000/api/w/workspace/scripts/list' \
-H 'Authorization: Bearer YOUR_TOKEN'
# Create a schedule
curl -X POST 'http://localhost:8000/api/w/workspace/schedules/create' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"path": "f/ops/daily_report",
"schedule": "0 9 * * *",
"args": {"channel": "#reports"}
}'
Approval Flows
# Workflow step that pauses for human approval
import wmill
def main(amount: float, vendor: str):
if amount > 1000:
# Pause workflow — sends approval request to manager
wmill.set_resume(
"Approve purchase of ${amount} from {vendor}?",
approvers=["manager@company.com"]
)
# Workflow pauses here until approved
# Continue with purchase order...
return {"po_number": "PO-2026-001", "amount": amount}
Build Internal Tools (No Frontend Code)
Windmill's app builder creates UIs from your scripts:
- Tables with filtering, sorting, pagination
- Forms that map to script inputs
- Charts and dashboards
- Buttons that trigger workflows
- Conditional visibility
Windmill vs Retool / n8n
| Windmill | Retool | n8n |
|---|---|---|
| Code-first | UI-first | Visual-first |
| Auto-generated UI | Build UI manually | No UI builder |
| Free self-hosted | $10/user/month | Free self-hosted |
| TypeScript/Python/Go | JS only | Visual + JS |
| Approval flows | Enterprise only | No approvals |
Need to automate data collection? Check out my web scraping actors on Apify — connect to Windmill for end-to-end automation.
Need custom internal tools? Email me at spinov001@gmail.com.
Top comments (0)