DEV Community

Alex Spinov
Alex Spinov

Posted on

Windmill Has a Free API: The Open-Source Retool Alternative That Turns Scripts Into Internal Tools, Workflows, and APIs

Your team needs an internal dashboard to manage users. A Retool license is $10/user/month. You could build it from scratch — but that's 2 weeks of React. Windmill lets you write a Python or TypeScript script and it automatically generates the UI, handles scheduling, logging, permissions, and error handling.

What Windmill Actually Does

Windmill is an open-source platform for building internal tools, workflows, and APIs from scripts. Write a function in Python, TypeScript, Go, Bash, SQL, or GraphQL — Windmill auto-generates a form UI from the function's parameters, handles execution, scheduling, retries, and audit logging.

Think of it as Retool + Temporal + Cron combined into one platform. Scripts become internal tools with auto-generated UIs. Chain scripts into workflows. Expose them as webhooks/APIs. Schedule them as cron jobs. All with version control, RBAC, and approval flows.

Self-hosted (free, open-source AGPLv3) or Windmill Cloud (free tier: 1,000 executions/month). The self-hosted version has no feature limitations.

Quick Start

# Self-hosted with Docker
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
docker compose up -d
# Open http://localhost:8000
Enter fullscreen mode Exit fullscreen mode

Create a script that becomes an instant internal tool:

# Windmill auto-generates a form from these parameters
def main(user_email: str, new_role: str = "member"):
    """Update a user's role in the database."""
    import psycopg2
    conn = psycopg2.connect("postgresql://...")
    cur = conn.cursor()
    cur.execute(
        "UPDATE users SET role = %s WHERE email = %s RETURNING id, email, role",
        (new_role, user_email)
    )
    result = cur.fetchone()
    conn.commit()
    return {"id": result[0], "email": result[1], "role": result[2]}
Enter fullscreen mode Exit fullscreen mode

Windmill creates: a web form with user_email text input and new_role dropdown, execution logging, permission controls, and a shareable link.

Or via API:

curl -X POST https://your-windmill.com/api/w/your-workspace/jobs/run/p/f/your-script \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_email": "alice@example.com", "new_role": "admin"}'
Enter fullscreen mode Exit fullscreen mode

3 Practical Use Cases

1. Automated Workflows

# Step 1: Fetch new signups
def get_new_signups():
    import requests
    response = requests.get("https://api.stripe.com/v1/customers",
        headers={"Authorization": "Bearer sk_..."},
        params={"created[gte]": int(time.time()) - 86400}
    )
    return response.json()["data"]

# Step 2: Enrich with Clearbit
def enrich_lead(customer):
    response = requests.get(f"https://company.clearbit.com/v2/companies/find?domain={customer['email'].split('@')[1]}")
    return {**customer, "company": response.json()}

# Step 3: Add to CRM
def add_to_crm(enriched_lead):
    requests.post("https://api.hubspot.com/crm/v3/objects/contacts",
        json={"properties": enriched_lead})
Enter fullscreen mode Exit fullscreen mode

Chain these in Windmill's visual flow editor. Runs daily.

2. Instant Webhook API

export async function main(req: {
  event: string;
  customer_id: string;
  amount: number;
}) {
  if (req.event === "payment.failed") {
    // Send Slack alert
    await fetch(SLACK_WEBHOOK, {
      method: "POST",
      body: JSON.stringify({
        text: `Payment failed for ${req.customer_id}: $${req.amount}`
      })
    });
    // Create support ticket
    await createZendeskTicket(req.customer_id, req.amount);
  }
  return { processed: true };
}
Enter fullscreen mode Exit fullscreen mode

Deploy as webhook with one click.

3. Scheduled Reports

def main():
    """Weekly revenue report — runs every Monday 9am."""
    import pandas as pd

    df = pd.read_sql("SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
                     "postgresql://...")

    summary = {
        "total_revenue": float(df["amount"].sum()),
        "order_count": len(df),
        "avg_order": float(df["amount"].mean()),
        "top_products": df.groupby("product")["amount"].sum().nlargest(5).to_dict()
    }

    # Send to Slack
    send_slack_message(format_report(summary))
    return summary
Enter fullscreen mode Exit fullscreen mode

Why This Matters

Windmill eliminates the build-vs-buy dilemma for internal tools. You don't need Retool's $10/user pricing or 2 weeks building a custom dashboard. Write a script, get an instant tool with proper logging, permissions, and scheduling. Self-hosted means your data stays on your infrastructure.


Need custom data extraction or web scraping solutions? I build production-grade scrapers and data pipelines. Check out my Apify actors or email me at spinov001@gmail.com for custom projects.

Follow me for more free API discoveries every week!

Top comments (0)