Windmill vs n8n vs Temporal for API automation
What You’ll Need
- n8n Cloud or self-hosted n8n instance
- Hetzner VPS or Contabo VPS for self-hosting options
- DigitalOcean as an alternative hosting provider
- Namecheap if you need a custom domain
- Basic understanding of REST APIs and webhooks
- A code editor (VS Code recommended)
- 30 minutes to follow along with examples
Table of Contents
- What is Windmill?
- What is n8n?
- What is Temporal?
- Head-to-Head Comparison
- Building Your First API Automation
- Performance Testing Real Scenarios
- Scaling Considerations
- Getting Started
What is Windmill?
I discovered Windmill about a year ago when I was building internal tools for a data team. It’s an open-source platform designed specifically for building internal apps, workflows, and scripts. Windmill emphasizes a low-code approach with strong Python and TypeScript support. The platform lets you write actual code—not just drag-and-drop logic—which appeals to engineers who want precision.
The core strength of Windmill lies in its script-first philosophy. You write scripts, compose them into flows, and trigger them via UI, API, or webhooks. It’s remarkably lightweight, which makes it ideal if you’re running it on a modest VPS from Hetzner VPS or Contabo VPS .
What I appreciate most: Windmill doesn’t lock you into a visual editor. You have full control over your code, and the platform handles scheduling, error handling, and API exposure automatically.
What is n8n?
n8n is the platform I reach for most often. It’s a workflow automation tool with a visual node-based editor, but—and this is crucial—you can drop into JavaScript whenever you need it. n8n Cloud removes hosting headaches, but the self-hosted version gives you complete control.
The advantage of n8n is its ecosystem. It comes with hundreds of pre-built integrations for popular services: Slack, Stripe, HubSpot, Airtable, you name it. This makes it faster to build common workflows without custom API calls.
I’ve used n8n for everything from syncing data between databases to automating customer onboarding flows. The visual editor is intuitive enough for non-technical team members, but powerful enough for complex orchestration logic.
What is Temporal?
Temporal is different from Windmill and n8n. It’s a distributed workflow engine built for microservices and complex long-running processes. If you’re building systems where tasks span hours, days, or even weeks—and failures need to be retried intelligently—Temporal is your answer.
I use Temporal when I need durability guarantees. It maintains workflow history, provides fault tolerance out of the box, and scales horizontally. It’s popular in fintech, payment processing, and systems that absolutely cannot lose work mid-execution.
However, Temporal is more complex to set up and requires you to write orchestration logic in your application code (Go, Java, Python, TypeScript). It’s not a SaaS platform with a UI—it’s infrastructure you deploy and integrate with your services.
Head-to-Head Comparison
Let me break down how these platforms stack up across key dimensions:
Ease of Use & Learning Curve
Windmill: Medium. You’re writing real code, so Python or TypeScript knowledge is required, but the platform handles execution, scheduling, and APIs automatically.
n8n: Low. Visual editor makes it accessible to non-developers. But you can drop into JavaScript when needed, so the ceiling is high.
Temporal: High. You must understand distributed systems concepts and write code in your chosen language. Setup involves cluster management and worker configuration.
Integration Library
Windmill: Growing, but smaller. You’ll write custom scripts to hit third-party APIs if a built-in connector doesn’t exist. This is actually fine if you prefer control.
n8n: Extensive. 400+ integrations out of the box. This is where n8n shines for typical SaaS automation.
Temporal: None. You implement all integrations in your worker code. This is intentional—Temporal is infrastructure, not a platform.
Self-Hosting & Infrastructure
Windmill: Lightweight. I’ve run it on a $5/month VPS with minimal resources. Great for Hetzner VPS deployments.
n8n: Moderate footprint. The self-hosted version needs a PostgreSQL database and reasonable CPU. Better on DigitalOcean droplets with 2GB+ RAM.
Temporal: Heavy. Needs a database (PostgreSQL or Cassandra), background workers, and potentially multiple services. Enterprise-grade infrastructure.
Performance & Throughput
Windmill: Handles hundreds of concurrent workflows on modest hardware. Node.js-based execution.
n8n: Similar to Windmill. Fine for most automation needs. Can hit bottlenecks at thousands of concurrent executions.
Temporal: Designed for millions of workflows. Horizontal scaling baked in. Built for extremely high concurrency.
Pricing Model
Windmill: Open-source and free to self-host. Cloud tier available but generous free tier.
n8n: n8n Cloud pricing starts at $40/month. Self-hosted is free.
Temporal: Self-hosted is free. Cloud offering exists but geared toward enterprise with custom pricing.
Building Your First API Automation
Let me show you practical examples with each platform. I’ll focus on a real scenario: polling a GitHub API every hour, extracting repository stats, and posting them to a webhook.
Windmill Example
Here’s how you’d do this in Windmill with a simple Python script and flow:
import requests
from datetime import datetime
def get_github_stats(username: str, token: str):
headers = {
"Authorization": f"token {token}",
"Accept": "application/vnd.github.v3+json"
}
url = f"https://api.github.com/users/{username}/repos"
response = requests.get(url, headers=headers)
response.raise_for_status()
repos = response.json()
total_stars = sum(repo.get("stargazers_count", 0) for repo in repos)
total_forks = sum(repo.get("forks_count", 0) for repo in repos)
repo_count = len(repos)
return {
"timestamp": datetime.utcnow().isoformat(),
"username": username,
"repo_count": repo_count,
"total_stars": total_stars,
"total_forks": total_forks,
"repositories": [
{
"name": repo["name"],
"stars": repo["stargazers_count"],
"forks": repo["forks_count"],
"url": repo["html_url"]
}
for repo in repos
]
}
def post_to_webhook(webhook_url: str, data: dict):
response = requests.post(webhook_url, json=data)
response.raise_for_status()
return {"success": True, "status_code": response.status_code}
In Windmill, you’d create two separate scripts, then compose them into a flow with a schedule trigger (hourly). The flow would pass the GitHub stats output directly to the webhook POST.
n8n Example
With n8n , you’d use the HTTP Request node. Here’s the configuration:
Node 1: GitHub API Request
{
"name": "Get GitHub Stats",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://api.github.com/users/{{ $json.github_username }}/repos",
"method": "GET",
"headers": {
"Authorization": "token {{ $env.GITHUB_TOKEN }}",
"Accept": "application/vnd.github.v3+json"
},
"responseFormat": "json"
}
}
Node 2: Transform Data
{
"name": "Calculate Stats",
"type": "n8n-nodes-base.code",
"parameters": {
"jsCode": "const repos = $json.body;\nconst totalStars = repos.reduce((sum, r) => sum + r.stargazers_count, 0);\nconst totalForks = repos.reduce((sum, r) => sum + r.forks_count, 0);\n\nreturn {\n timestamp: new Date().toISOString(),\n repo_count: repos.length,\n total_stars: totalStars,\n total_forks: totalForks,\n repositories: repos.map(r => ({\n name: r.name,\n stars: r.stargazers_count,\n forks: r.forks_count,\n url: r.html_url\n }))\n};"
}
}
Node 3: Send to Webhook
{
"name": "Post Results",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "{{ $env.WEBHOOK_URL }}",
"method": "POST",
"contentType": "application/json",
"body": "{{ JSON.stringify($json) }}"
}
}
Then set a Cron trigger for hourly execution. The beauty here is no code required—n8n handles the HTTP, transformation, and scheduling through the UI.
Temporal Example
Temporal requires a different approach. You’d write a workflow and activity:
package main
import (
"context"
"encoding/json"
"io"
"net/http"
"time"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/workflow"
)
type GitHubStats struct {
Timestamp string `json:"timestamp"`
Username string `json:"username"`
RepoCount int `json:"repo_count"`
TotalStars int `json:"total_stars"`
TotalForks int `json:"total_forks"`
Repositories []Repository `json:"repositories"`
}
type Repository struct {
Name string `json:"name"`
Stars int `json:"stars"`
Forks int `json:"forks"`
URL string `json:"url"`
}
type GitHubRepo struct {
Name string `json:"name"`
StargazersCount int `json:"stargazers_count"`
ForksCount int `json:"forks_count"`
HTMLURL string `json:"html_url"`
}
func GetGitHubStats(ctx context.Context, username string, token string) (*GitHubStats, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/users/"+username+"/repos", nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "token "+token)
req.Header.Add("Accept", "application/vnd.github.v3+json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.