Building Serverless Automation with n8n vs Temporal for Developers
What You’ll Need
- n8n Cloud or self-hosted n8n
- Hetzner VPS or Contabo VPS for hosting
- DigitalOcean as alternative
- Node.js 18+ (for local development)
- Docker (optional, for containerized Temporal)
- Basic understanding of APIs and workflows
Table of Contents
- What Serverless Automation Really Means
- n8n: Visual Workflows Without Code
- Temporal: Durable Execution for Complex Tasks
- Direct Comparison: When to Use Each
- Building Your First n8n Workflow
- Building Your First Temporal Workflow
- Real-World Example: Order Processing Pipeline
- Getting Started
What Serverless Automation Really Means
I’ve spent the last three years automating backend processes, and I can tell you that “serverless” means something specific in this context: you’re not managing infrastructure, but you are orchestrating logic that runs reliably without your constant intervention.
The challenge developers face isn’t just building automation—it’s building automation that survives network failures, retries gracefully, and doesn’t lose state when things break. That’s where n8n and Temporal diverge dramatically.
n8n handles workflow automation through a visual interface with built-in integrations. Temporal handles distributed, long-running tasks with fault tolerance baked into the runtime. They’re solving adjacent problems, and choosing between them depends entirely on your use case.
I’ve implemented both in production. Let me show you how they work and where each excels.
n8n: Visual Workflows Without Code
n8n is a workflow automation platform I reach for when I need to connect APIs and services quickly. It’s Node-based, meaning you build by connecting boxes rather than writing boilerplate.
Here’s what makes n8n compelling: it handles webhook fundamentals natively. Every workflow can expose an HTTP endpoint, making it easy to trigger from external services.
Let me show you a basic workflow structure. You’ll define workflows in JSON, and n8n Cloud handles execution, or you host it yourself on Hetzner VPS or DigitalOcean .
Here’s a simple webhook-triggered workflow that processes incoming data:
{
"name": "Process Customer Order",
"nodes": [
{
"parameters": {
"path": "orders",
"responseMode": "onReceived"
},
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"method": "POST",
"url": "https://api.example.com/validate",
"authentication": "genericCredentialType",
"genericCredentialType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer {{ $secrets.API_KEY }}"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "order_id",
"value": "{{ $json.body.orderId }}"
},
{
"name": "customer_email",
"value": "{{ $json.body.email }}"
}
]
}
},
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [450, 300]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "{{ $json.body.valid }}",
"operator": "equals",
"value2": true
}
]
}
},
"name": "Check Validity",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [650, 300]
},
{
"parameters": {
"authentication": "oAuth2",
"resource": "message",
"operation": "send",
"email": "{{ $json.body.email }}",
"subject": "Order Confirmed",
"messageText": "Your order #{{ $json.body.orderId }} has been confirmed."
},
"name": "Send Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [850, 300]
}
],
"connections": {
"Webhook": {
"main": [[{ "node": "HTTP Request", "branch": 0, "type": "main" }]]
},
"HTTP Request": {
"main": [[{ "node": "Check Validity", "branch": 0, "type": "main" }]]
},
"Check Validity": {
"main": [
[{ "node": "Send Email", "branch": 0, "type": "main" }],
[]
]
}
}
}
This workflow receives an order via webhook, validates it against an external API, checks if validation passed, and sends a confirmation email. The entire logic is declarative and runs managed for you.
n8n excels when you’re orchestrating SaaS tools and APIs. It’s less about complex business logic and more about “connect these services together.” I’ve used it extensively for tasks like automating social media posts with n8n , syncing databases, and triggering notifications.
The limitation? n8n isn’t designed for long-running, fault-tolerant operations. If your workflow takes 30 minutes to complete and fails halfway through, you need custom retry logic. It’s execution is stateless—each run is independent.
Temporal: Durable Execution for Complex Tasks
Temporal solves a fundamentally different problem. It’s a platform for running durable, distributed workflows with guaranteed execution and built-in failure handling.
Unlike n8n’s visual approach, Temporal requires you to write code. But that code is what I’d call “honest”—it looks like normal imperative logic, and Temporal handles all the infrastructure.
Here’s a basic Temporal workflow that processes a payment order with retries and timeouts:
import { proxyActivities, defineSignal, defineQuery, WorkflowExecutionAlreadyStartedError } from '@temporalio/workflow';
import type * as activities from './activities';
const { chargePayment, notifyCustomer, refundPayment, updateInventory } = proxyActivities<typeof activities>({
startToCloseTimeout: '10 minutes',
retry: {
initialInterval: '1s',
maximumInterval: '1 minute',
maximumAttempts: 5,
},
});
export interface OrderInput {
orderId: string;
customerId: string;
amount: number;
email: string;
items: { sku: string; quantity: number }[];
}
export interface OrderState {
status: 'pending' | 'charged' | 'inventory_updated' | 'notified' | 'failed';
errorMessage?: string;
}
let currentState: OrderState = { status: 'pending' };
export const updateOrderStatus = defineSignal<[OrderState]>('updateOrderStatus');
export const getOrderStatus = defineQuery<OrderState>('getOrderStatus');
export async function processOrder(input: OrderInput): Promise<OrderState> {
try {
currentState.status = 'pending';
await chargePayment({
customerId: input.customerId,
amount: input.amount,
orderId: input.orderId,
});
currentState.status = 'charged';
await updateInventory({
orderId: input.orderId,
items: input.items,
});
currentState.status = 'inventory_updated';
await notifyCustomer({
email: input.email,
orderId: input.orderId,
subject: 'Order Confirmed',
body: `Your order has been processed. Total: $${input.amount}`,
});
currentState.status = 'notified';
return currentState;
} catch (error) {
currentState.status = 'failed';
currentState.errorMessage = error instanceof Error ? error.message : String(error);
try {
await refundPayment({
customerId: input.customerId,
orderId: input.orderId,
});
} catch (refundError) {
console.error('Refund failed:', refundError);
}
throw error;
}
}
And the activities (functions that do the actual work):
import { Context } from '@temporalio/activity';
export async function chargePayment(input: {
customerId: string;
amount: number;
orderId: string;
}): Promise<{ transactionId: string }> {
const controller = new AbortController();
const timeoutHandle = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch('https://api.payment.example.com/charge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PAYMENT_API_KEY}`,
},
body: JSON.stringify({
customer_id: input.customerId,
amount: input.amount,
order_id: input.orderId,
idempotency_key: `${input.orderId}-charge`,
}),
signal: controller.signal,
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Payment API error: ${response.status} - ${error}`);
}
const data = await response.json() as { transaction_id: string };
return { transactionId: data.transaction_id };
} finally {
clearTimeout(timeoutHandle);
}
}
export async function updateInventory(input: {
orderId: string;
items: { sku: string; quantity: number }[];
}): Promise<void> {
const response = await fetch('https://api.inventory.example.com/reserve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.INVENTORY_API_KEY}`,
},
body: JSON.stringify({
order_id: input.orderId,
items: input.items,
}),
});
if (!response.ok) {
throw new Error(`Inventory API error: ${response.status}`);
}
}
export async function notifyCustomer(input: {
email: string;
orderId: string;
subject: string;
body: string;
}): Promise<void> {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
},
body: JSON.stringify({
personalizations: [
{
to: [{ email: input.email }],
},
],
from: { email: 'orders@
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.