Workflow automation is the infrastructure that makes AI agents reliable. Without durable execution, a network hiccup during a 10-step payment flow means lost money and angry customers. With it, workflows resume exactly where they left off. This skill covers the platforms (n8n, Temporal, Inngest) and patterns (sequential, parallel, orchestrator-worker) that turn brittle scripts into production-grade automation. Key insight: The platforms make different tradeoffs. n8n optimizes for accessibility
Workflow automation is the infrastructure that makes AI agents reliable.
Without durable execution, a network hiccup during a 10-step payment
flow means lost money and angry customers. With it, workflows resume
exactly where they left off.
This skill covers the platforms (n8n, Temporal, Inngest) and patterns
(sequential, parallel, orchestrator-worker) that turn brittle scripts
into production-grade automation.
Key insight: The platforms make different tradeoffs. n8n optimizes for
accessibility, Temporal for correctness, Inngest for developer experience.
Pick based on your actual needs, not hype.
Principles
Durable execution is non-negotiable for money or state-critical workflows
Events are the universal language of workflow triggers
Steps are checkpoints - each should be independently retryable
Start simple, add complexity only when reliability demands it
Observability isn't optional - you need to see where workflows fail
"""
export async function orchestratorWorkflow(task: ComplexTask) {
// Orchestrator decides what work needs to be done
const plan = await analyzeTask(task);
"""
// Schedule workflow to run on cron
const handle = await client.workflow.start(dailyReportWorkflow, {
taskQueue: 'reports',
workflowId: 'daily-report',
cronSchedule: '0 9 * * *', // 9 AM daily
});
"""
n8n Schedule Trigger
"""
[Schedule Trigger: Every day at 9:00 AM]
↓
[HTTP Request: Get Metrics]
↓
[Code Node: Generate Report]
↓
[Send Email: Report]
"""
Sharp Edges
Non-Idempotent Steps in Durable Workflows
Severity: CRITICAL
Situation: Writing workflow steps that modify external state
Symptoms:
Customer charged twice. Email sent three times. Database record
created multiple times. Workflow retries cause duplicate side effects.
Why this breaks:
Durable execution replays workflows from the beginning on restart.
If step 3 crashes and the workflow resumes, steps 1 and 2 run again.
Without idempotency keys, external services don't know these are retries.
await db.query( INSERT INTO orders (id, ...) VALUES ($1, ...) ON CONFLICT (id) DO NOTHING, [orderId]);
Generate idempotency key from stable inputs, not random values
Workflow Runs for Hours/Days Without Checkpoints
Severity: HIGH
Situation: Long-running workflows with infrequent steps
Symptoms:
Memory consumption grows. Worker timeouts. Lost progress after
crashes. "Workflow exceeded maximum duration" errors.
Why this breaks:
Workflows hold state in memory until checkpointed. A workflow that
runs for 24 hours with one step per hour accumulates state for 24h.
Workers have memory limits. Functions have execution time limits.
Recommended fix:
Break long workflows into checkpointed steps:
WRONG - one long step:
await step.run("process-all", async () => {
for (const item of thousandItems) {
await processItem(item); // Hours of work, one checkpoint
}
});
CORRECT - many small steps:
for (const item of thousandItems) {
await step.run(process-${item.id}, async () => {
return processItem(item); // Checkpoint after each
});
}
For very long waits, use sleep:
await step.sleep("wait-for-trial", "14 days");
// Doesn't consume resources while waiting
Situation: Calling external services from workflow activities
Symptoms:
Workflows hang indefinitely. Worker pool exhausted. Dead workflows
that never complete or fail. Manual intervention needed to kill stuck
workflows.
Why this breaks:
External APIs can hang forever. Without timeout, your workflow waits
forever. Unlike HTTP clients, workflow activities don't have default
timeouts in most platforms.
Situation: Writing code that runs during workflow replay
Symptoms:
Random failures on replay. "Workflow corrupted" errors. Different
behavior on replay than initial run. Non-determinism errors.
Why this breaks:
Workflow code runs on EVERY replay. If you generate a random ID in
workflow code, you get a different ID each replay. If you read the
current time, you get a different time. This breaks determinism.
Recommended fix:
WRONG - side effects in workflow code:
export async function orderWorkflow(order) {
const orderId = uuid(); // Different every replay!
const now = new Date(); // Different every replay!
await activities.process(orderId, now);
}
CORRECT - side effects in activities:
export async function orderWorkflow(order) {
const orderId = await activities.generateOrderId(); # Recorded
const now = await activities.getCurrentTime(); # Recorded
await activities.process(orderId, now);
}
Also CORRECT - Temporal workflow.now() and sideEffect:
import { sideEffect } from '@temporalio/workflow';
const orderId = await sideEffect(() => uuid());
const now = workflow.now(); # Deterministic replay-safe time
Side effects that are safe in workflow code:
- Reading function arguments
- Simple calculations (no randomness)
- Logging (usually)
Retry Configuration Without Exponential Backoff
Severity: MEDIUM
Situation: Configuring retry behavior for failing steps
Symptoms:
Overwhelming failing services. Rate limiting. Cascading failures.
Retry storms causing outages. Being blocked by external APIs.
Why this breaks:
When a service is struggling, immediate retries make it worse.
100 workflows retrying instantly = 100 requests hitting a service
that's already failing. Backoff gives the service time to recover.
Why this breaks:
Workflow state is persisted and replayed. A 10MB payload is stored,
serialized, and deserialized on every step. This adds latency and
cost. Some platforms have hard limits (e.g., Step Functions 256KB).
Symptoms:
Failed workflows silently disappear. No alerts when things break.
Customer issues discovered days later. Manual recovery impossible.
Why this breaks:
Even with retries, some workflows will fail permanently. Without
dead letter handling, you don't know they failed. The customer
waits forever, you're unaware, and there's no data to debug.
Symptoms:
Workflow fails silently. Errors only visible in execution logs.
No alerts, no recovery, no visibility until someone notices.
Why this breaks:
n8n doesn't notify on failure by default. Without an Error Trigger
node connected to alerting, failures are only visible in the UI.
Production failures go unnoticed.
Consider dead letter pattern:
[Error Trigger]
↓
[Redis/Postgres: Store Failed Job]
↓
[Separate Recovery Workflow]
Also use:
Retry on node failures (built-in)
Node timeout settings
Workflow timeout
Long-Running Temporal Activities Without Heartbeat
Severity: MEDIUM
Situation: Activities that run for more than a few seconds
Symptoms:
Activity timeouts even when work is progressing. Lost work when
workers restart. Can't cancel long-running activities.
Why this breaks:
Temporal detects stuck activities via heartbeat. Without heartbeat,
Temporal can't tell if activity is working or stuck. Long activities
appear hung, may timeout, and can't be gracefully cancelled.
Recommended fix:
For any activity > 10 seconds, add heartbeat:
import { heartbeat, activityInfo } from '@temporalio/activity';
for (let i = 0; i < chunks.length; i++) {
// Check for cancellation
const { cancelled } = activityInfo();
if (cancelled) {
throw new CancelledFailure('Activity cancelled');
}