PulsePost: I built an AI pipeline that writes, approves, and publishes articles for me
I got tired of the gap between “I should write about this” and actually writing it. So I built PulsePost — an event-driven pipeline that monitors AI trends, drafts articles in my voice, asks for my approval via Telegram, and publishes directly to this blog.
Here’s how it works.
The pipeline
Five Azure Functions wired together through Service Bus queues:
flowchart TD
T1([Timer\nSunday 22:00 UTC]) --> SBQ1
T2([Telegram\n/generate]) --> WH[TelegramWebhook]
WH --> SBQ1
SBQ1[(Service Bus\nfetch-topics)] --> FT[FetchTopics]
FT -->|HN + Blogs + YouTube + arXiv| SBQ2
SBQ2[(Service Bus\ngenerate-article)] --> GA[GenerateArticle]
GA -->|3x GPT-4o calls| OAI[Azure OpenAI]
OAI --> GA
GA -->|pending draft| TS[(Table Storage\nArticleDrafts)]
GA -->|preview| BOT([Telegram Bot])
BOT --> H([Harry])
H -->|APPROVE / REJECT / EDIT| WH2[TelegramWebhook]
WH2 -->|REJECT| TS
WH2 -->|EDIT: rewrite + re-notify| OAI
WH2 -->|APPROVE: rowKey| SBQ3
SBQ3[(Service Bus\npost-article)] --> PA[PostArticle]
PA -->|branch + commit + PR| GH[(GitHub)]
PA -->|X thread + image prompt| BOT
PA -->|approved| TS
Every Sunday at 10pm UTC, a timer trigger fires and drops a message into the fetch-topics queue. Everything after that is reactive — no polling, no cron juggling, just messages flowing through queues.
Topic fetching
TopicFetchService hits four sources in parallel:
- Hacker News top stories — filtered by AI keywords (
llm,agent,mcp,rag,claude,openai, etc.) - Company RSS feeds — OpenAI, Google DeepMind, Meta AI blogs
- YouTube feeds — Karpathy, Yannic Kilcher, Two Minute Papers, Lex Fridman, and others via Atom XML
- arXiv — latest
cs.AIpapers
All four fetches run concurrently with Task.WhenAll. The combined JSON goes straight into the generate-article queue.
Article generation
GenerateArticle calls Azure OpenAI three times in sequence:
- Topic selection — GPT-4o picks the single most interesting topic from the raw feed dump and returns structured JSON with title, angles, why-now context, and source links
- Article draft — 800-1200 words in my voice, with code examples in C#, Python, or TypeScript depending on the topic
- X thread — 6-8 tweets reformatted from the article, plus an image prompt for DALL-E
The style prompt I feed GPT-4o is blunt:
Harry writes in first-person, direct, opinionated tone. Technical but accessible.
He uses real-world examples, often from banking or logistics systems.
He avoids fluff — every paragraph makes a point.
Short sentences. Occasional rhetorical questions. No corporate speak.
The draft lands in Azure Table Storage with status pending, then I get a Telegram message with the topic, source links, a two-tweet preview, and the image prompt.
The approval gate
This is the part I actually care about. I don’t want to auto-publish without reading it.
The TelegramWebhook Azure Function receives HTTP POST from Telegram and handles three commands:
APPROVE → enqueues to post-article, publishes
REJECT → marks draft rejected, discarded
EDIT <notes> → rewrites with feedback, sends revised preview
The EDIT flow is genuinely useful. I might reply EDIT make it more focused on the distributed systems angle and get a revised draft in under a minute. I can iterate as many times as I want before approving.
There’s also a /generate command to manually kick off the pipeline at any time — useful when something breaks in the news mid-week.
Security: the webhook checks TELEGRAM_CHAT_ID before processing any message. Unknown senders are silently dropped.
Publishing
Once approved, PostArticle calls the GitHub Contents API to create the .md file directly in _posts/, then opens a pull request. GitHub Pages builds and deploys automatically on merge.
The Telegram confirmation includes the full X thread ready to copy-paste, and the DALL-E image prompt. I still generate the cover image manually and attach it before merging — that’s the one step I haven’t automated yet.
Infrastructure as Code
The entire Azure environment is provisioned with Terraform and Bicep:
- Azure Function App (Consumption plan)
- Service Bus namespace with three queues (
fetch-topics,generate-article,post-article) - Storage Account (Function host + Azure Table Storage for drafts)
- Application Insights for observability
Both Terraform and Bicep provision the same infrastructure — two IaC approaches side by side, useful for comparing syntax and tooling on a real project.
flowchart LR
SRC[Push to src/** or tests/**\nor pull request to main] --> CI
subgraph CI["ci.yml — GitHub Actions"]
B1[dotnet restore] --> B2[dotnet build\n-c Release] --> B3[dotnet test\n--no-build]
end
CI --> R([Pass / Fail])
GitHub Actions runs a single CI workflow on every push or PR touching src/ or tests/. Deploy and infrastructure provisioning are run manually.
What I learned
Service Bus over HTTP chaining. I could have chained these functions with HTTP calls, but queues give me retry logic, dead-lettering, and natural backpressure for free. If the OpenAI call fails, the message stays in the queue and retries — no custom retry logic needed.
Azure Table Storage is underrated. For a simple draft store with a handful of columns, it’s free tier, zero config, and native to the Functions ecosystem. I don’t need a full database for this.
The approval gate is the feature. Auto-publishing without review would be fast but risky. The Telegram loop keeps me in control without adding friction — I can approve from my phone in 30 seconds.
The full source is on GitHub.