Automated Content Engine — Claude + HeyGen + n8n at scale
Replace 15+ hours of weekly video production with a self-improving multi-tenant pipeline.

The problem
A growing agency was producing weekly social videos for multiple brands. The bottleneck wasn't HeyGen rendering — it was the decision-making before rendering:
- Which topic? Pulled from spreadsheets, manually
- What script? Drafted in Google Docs, edited 3+ rounds
- What hook? Best-guess based on yesterday's "feel"
- Which platforms? Posted manually one by one
Each brand was eating 15+ hours of human time per week. Multi-tenant scaling was impossible without automation.
The approach
A NestJS + BullMQ pipeline that owns the full lifecycle, with feedback loops from real social engagement data feeding back into script generation.
Why NestJS (and not just n8n end-to-end)
n8n was perfect for distribution — it speaks every social platform's API natively, retries cleanly, and the agency's ops team could maintain it. But putting the core pipeline in n8n was the wrong call:
- Domain logic complexity. Multi-tenant prompt assembly, feedback-loop math, tenant-specific guardrails — these wanted real code.
- Queue semantics. BullMQ's retry, rate-limit, and priority controls beat n8n's job model for HeyGen renders that can take 5–10 minutes.
- Test coverage. Jest tests on the prompt builder caught more than a dozen tenant-data leaks before they shipped.
So: NestJS owns the pipeline. n8n owns the last mile.
The pipeline isn't static. Every video's engagement (views, watch time, saves, comments) updates a per-tenant feature_weights record. The next script generation reads those weights as soft constraints — favoring the hook structures and topics that performed.
Multi-tenant data isolation
Every job carries a tenant_id. Every prompt assembly, every queue read, every metric write asserts it. Two safeguards:
- Type-system enforcement. A
TenantScoped<T>wrapper makes it impossible to pass a tenant-naked object into a generator. - Logged guardrails. Each generation logs the tenant context; a per-tenant audit log catches any drift.
BullMQ topology
- Topic queue — low priority, runs nightly per tenant
- Script queue — medium priority, reads topics
- Render queue — high priority but rate-limited (HeyGen quota)
- Distribution queue — handed to n8n via webhook
Outcomes
What this taught me
Building this convinced me of one thing: the AI part is the easy part now. The hard part is everything around it — multi-tenant isolation, retry semantics, feedback loops, ops maintainability, and knowing when to use no-code (n8n) vs typed code (NestJS) for each layer.
If you're building something like this and aren't sure where the n8n / NestJS split should land, that's exactly the conversation I love to have.
Building something similar?
Send a quick note — happy to compare notes on the architecture.