DEV Community

Subhendu Das
Subhendu Das

Posted on

Developer Marketing Automation in Herald

The mechanics of automated publishing lifecycles

Turning merged pull requests into published technical articles requires more than generating text. For solo developers and small teams, developer marketing usually stalls between drafting and distribution. Even when drafts are produced automatically from repository changes, managing publication dates across platforms like Dev.to and Medium introduces non-trivial state management challenges.

A resilient content calendar cannot operate as a basic cron script. Content transitions through distinct phases: drafting, team review, arming, platform dispatch, and post-publication tracking. If the underlying state machine permits inconsistent transitions—such as leaving a demoted draft armed for dispatch or allowing an archived item to overwrite live content—automated publication pipelines fail silently or release unintended drafts.

Herald addresses this distribution bottleneck by pairing an asynchronous worker architecture with a strictly guarded state engine designed to schedule and deliver technical posts reliably.

How Herald's content calendar and state engine work

Herald is built on FastAPI, PostgreSQL, SQLAlchemy, Celery, and Redis, with a frontend delivered through React, Vite, and Tailwind CSS. While AI content generation watches git commits to draft copy when features ship, the release process is controlled by Herald's scheduling backend.

Content scheduling relies on explicit state transitions managed inside PostgreSQL using SQLAlchemy. Posts move sequentially from a generated draft to a scheduled slot on the content calendar, waiting for Celery workers to execute the final dispatch:

# backend/models/post.py
from enum import Enum
from sqlalchemy import Column, Integer, String, DateTime, Enum as SQLEnum
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class PostStatus(str, Enum):
    DRAFT = "draft"
    REVIEW = "review"
    SCHEDULED = "scheduled"
    ARMED = "armed"
    PUBLISHED = "published"
    ARCHIVED = "archived"

class Post(Base):
    __tablename__ = "posts"

    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    content = Column(String, nullable=False)
    scheduled_for = Column(DateTime, nullable=True)
    status = Column(SQLEnum(PostStatus), default=PostStatus.DRAFT, nullable=False)
Enter fullscreen mode Exit fullscreen mode

Recent fixes to Herald's state engine resolve edge cases that traditionally destabilize marketing automation pipelines. For example, when an item was demoted from a scheduled state, Celery task pointers could remain armed, risking accidental deployment. Herald enforces strict guards across CRUD routers and asynchronous workers so that changes in state explicitly synchronize with the worker queue:

# backend/services/scheduler.py
from datetime import datetime, timezone
from sqlalchemy.orm import Session

def disarm_post(db: Session, post: Post) -> None:
    if post.status == PostStatus.ARMED:
        post.status = PostStatus.DRAFT
        post.scheduled_for = None
        db.commit()
Enter fullscreen mode Exit fullscreen mode

When a post reaches its target release window, Celery workers poll Redis for armed jobs, resolve credentials, and execute external calls against publishing endpoints such as the Devto API.

Managing the schedule safely

To manage scheduling without accidental dispatches or broken preview links, Herald isolates the preview, approval, and scheduling endpoints inside FastAPI.

When reviewing generated technical posts before queuing them:

  1. Developers review the generated markdown and platform-specific metadata inside the React interface.
  2. Preview links can be generated and subsequently revoked cleanly without leaking stale responses.
  3. The developer picks a distribution slot, moving the post from review to scheduled.
  4. When armed, delivery routines wrap outgoing API calls inside strict exception guards to prevent worker crashes when third-party endpoints reject machine credentials or throttle requests.
# backend/tasks/delivery.py
import logging
from celery import shared_task
from backend.services.devto import publish_to_devto

logger = logging.getLogger(__name__)

@shared_task(bind=True, max_retries=3)
def deliver_post_task(self, post_id: int):
    try:
        post = fetch_post_by_id(post_id)
        if post.status != PostStatus.ARMED:
            return False

        response = publish_to_devto(post)
        post.status = PostStatus.PUBLISHED
        commit_post(post)
        return True
    except Exception as exc:
        logger.error("Dispatch failed for post %s: %s", post_id, exc)
        raise self.retry(exc=exc, countdown=60)
Enter fullscreen mode Exit fullscreen mode

By formalizing state rules—ensuring archived posts cannot be overwritten by restore operations, guarding webhook execution against unhandled errors, and sanitizing metric polls so tokens never reach backend logs—Herald ensures that developers can leave marketing automation running in the background without worrying about errant or duplicate releases.

Top comments (0)