DEV Community

Roberto Luna
Roberto Luna

Posted on

I Built an AI Pipeline to Write About Building My Products. Then I Had to Debug the Debugger.

I Built an AI Pipeline to Write About Building My Products. Then I Had to Debug the Debugger.

There's a particular kind of vertigo that hits when you're three hours deep into fixing a content automation system, and you realize the bug you're fixing would make a better blog post than anything the system has generated so far.

That's where I am right now. Let me back up.

The problem nobody asked me to solve

I'm a solo full-stack developer running four SaaS products out of Playa del Carmen, Mexico — a real estate CRM, a laundry service CRM, an industrial ERP, and a wellness knowledge base. All shipping, all in production, all demanding constant iteration. Somewhere in the middle of Sprint 3 on one of them, I had the thought every indie hacker has eventually: I should be building in public.

The problem is building in public requires writing. Writing requires time. Time is the one resource that doesn't scale when you're the only engineer on four codebases.

So I did what felt like the obvious move: I built a pipeline that watches my GitHub activity, generates technical articles from real commits, and publishes them across Dev.to, Medium, Substack, and Bluesky — automatically, every day, in two languages.

It worked. Mostly. And the parts where it didn't work taught me more about distributed systems than the actual CRM work did that week.

Finding a brain that wouldn't run out of credits

The first wall I hit wasn't technical — it was economic. My initial plan was Claude for content generation, since I already use it for development. Insufficient API credits. Fine, pivot to Gemini.

Gemini's API setup turned into a maze: my Google Cloud organization policy was issuing OAuth tokens instead of static API keys, which meant every token expired in about an hour. For a daily cron job, an hourly-expiring credential is not infrastructure — it's a liability with a timer on it.

I ended up on Groq, running llama-4-scout-17b-16e-instruct with a free tier that gives me 14,400 requests a day. It's not the same caliber of model I use for actual coding work, but for generating a daily changelog summary from commit diffs, it's more than enough — and it doesn't expire on me mid-deploy.

The lesson here wasn't "pick Groq." It was: the model you can depend on beats the model that's marginally smarter, especially for unattended automation. I'd rather have a 17B parameter model that runs reliably at 9am every day than a frontier model whose credentials I have to babysit.

The architecture, once it stabilized

GitHub Activity (4 repos)
        │
        ▼
  GitHub Discovery + Relevance Filter
   (ignores typo/whitespace-only commits)
        │
        ▼
   Topic Classifier
        │
        ▼
  Content Generator (Groq)
   ├─ Medium ES/EN
   ├─ Substack ES/EN
   ├─ Dev.to EN (technical deep-dive prompt)
   ├─ Bluesky thread ES/EN + image card
   └─ Changelog
        │
        ▼
  Platform Publishers
   ├─ Dev.to API
   ├─ Bluesky API (AT Protocol)
   ├─ Substack (cookie-based)
   ├─ Craft Docs (REST, partial)
   └─ Email digest + ClickUp tasks
Enter fullscreen mode Exit fullscreen mode

Three GitHub Actions workflows run on staggered crons — Dev.to at 9am Cancún, Bluesky and the Medium/Substack/email batch five minutes apart at 11am. That stagger turned out to matter more than I expected, which is the next story.

When the scheduler lies to you

Here's something the GitHub Actions documentation buries: if you update a cron schedule via a push, the new schedule can take 15 minutes to over an hour to register — and the first trigger after that change can silently skip entirely. No error. No failed run. Just... nothing happens, and the only signal is the absence of an email you were expecting.

I lost a full day of Bluesky and Substack publishing to this. Two workflows, same 0 17 * * * cron, pushed in the same commit. GitHub registered one, dropped the other. The fix wasn't in my code — it was offsetting the crons by five minutes each (5 17 * * *, 10 17 * * *) and pushing a trivial commit to force a schedule resync.

This is the kind of bug that doesn't show up in any framework's "getting started" guide, because it's not about your code being wrong. It's about a black-box scheduler having undocumented behavior under specific conditions — same-minute triggers, recent cron edits. You only find it by watching the absence of expected behavior and being suspicious enough to dig.

The REST API that almost, but doesn't, do what its docs say

The hardest bug — the one I'm mid-fight with as I write this — lives in Craft Docs' public REST API. I wanted generated articles to land, fully written, directly into a "ready to publish" folder I review before posting anywhere.

The API docs are explicit: "Use PUT /documents/move to move documents between locations." I built against that. Got a 400 with "expected":"object", "received":"undefined" — a Zod validation error with an empty path, meaning the entire request body shape was wrong, not just a field.

I tried documentIds as an array. 400. I found a second endpoint, PUT /blocks/move, with actual documented request/response examples in the OpenAPI spec — moving block IDs with a position.pageId target. Tried that against a folder ID. 404, "Block not found." Because a folder isn't a block. The endpoint moves content within documents, not documents between folders.

I tested whether parentId on document creation would sidestep the whole move operation. Confirmed via Claude's own MCP integration with Craft that creating-with-folder works somewhere in their stack — just not through the public REST surface. The parameter is silently ignored server-side.

Conclusion, after exhausting every documented and undocumented combination: Craft's public API can create content and it can read content, but placing that content into a specific folder requires their interactive MCP layer — which only runs inside their own client, not from a CI runner. There's no server-to-server credential that unlocks it.

So now the pipeline writes complete, formatted articles into Craft's default unsorted bucket, and I drag them into place by hand. Not elegant. But here's the thing — I stopped trying to solve it. Three different request shapes, two different endpoints, official documentation that contradicted observed behavior. At some point the engineering call isn't "find the fourth approach," it's "accept the thirty-second manual step and move on to work that actually compounds."

The part I almost shipped without thinking

While debugging all of this, I noticed something in my own code that bothered me more than any API quirk: auto_publish=True, hardcoded, on both the Dev.to and Bluesky publishers. Medium and Substack were correctly gated behind draft_only: true — but somehow the two channels that publish instantly, with no review step, were the two channels with zero human checkpoint between "LLM generates text" and "it's live under my name."

That's backwards. A 17B model summarizing git diffs is good enough for a daily digest — it is not good enough to publish unreviewed to a technical audience that will judge my actual product based on the writing quality.

Fixed it the same day I noticed it:

bluesky:
  auto_publish: false  # Requires review before posting

devto:
  auto_publish: false  # Requires review before posting
Enter fullscreen mode Exit fullscreen mode

The content still generates daily, still gets archived, still shows up for me to read. It just doesn't go anywhere until I say so. The automation's job is to remove the writing bottleneck, not the judgment bottleneck.

What four weeks of this actually taught me

None of these bugs were exotic. A scheduler with undocumented timing behavior. A REST API whose docs don't match its validation logic. My own code defaulting to "ship immediately" instead of "ask first." These are the unglamorous failure modes every backend engineer has hit — I just hit all three in the same system, in the same week, while building something whose entire purpose was to write about hitting them.

If you're building your own build-in-public pipeline, or honestly any unattended automation that touches external APIs and publishes things: budget real time for the scheduler lying to you, the API docs being aspirational rather than accurate, and your own code being more permissive than you intended. None of that shows up in the demo. All of it shows up in week three.


Part of my Build in Public series — sharing the real process of building four SaaS products from Playa del Carmen, México.

Repo: zaerohell/content-automation

Roberto Luna Osorio – Full Stack Developer & Project Lead
Playa del Carmen, México


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-06-30

#playadev #buildinpublic

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

The debugger-debugger point is the real lesson. Once content production is automated, the bottleneck moves to evaluation: what gets rejected, what gets rewritten, and what trace proves the pipeline did not just publish fluent noise.