DEV Community

Germey
Germey

Posted on

The Hard Part of Automated Publishing Isn’t Writing—It’s Delivery

Content publishing automation usually looks easy in a diagram. A draft goes in, an API call goes out, and a post appears on a blog or social network.

The real workflow is rarely that tidy.

One platform wants OAuth, another expects an API key, and a third depends on a browser session that may expire without warning. Some APIs create a draft while others publish immediately. A successful HTTP response may mean “accepted for processing,” not “visible to readers.” Then there is the uncomfortable question every team eventually asks: what happens when the job retries after a timeout?

That is why I think the useful abstraction is not “one-click publishing.” It is a small, observable delivery pipeline. The AI-written draft is only one input. Authentication, policy, state, and recovery are the parts that make the system dependable.

Treat every destination as a different system

It is tempting to hide every content platform behind one generic publish() function. That interface is convenient, but pretending the destinations behave identically creates fragile code.

A better connector keeps a shared contract while exposing platform-specific capabilities. The shared layer can describe the article, desired visibility, tags, canonical URL, and media. The adapter can then decide whether the target supports drafts, scheduling, cover images, or direct publication.

type DeliveryRequest = {
  title: string
  markdown: string
  tags: string[]
  visibility: "draft" | "public"
  canonicalUrl?: string
  idempotencyKey: string
}

type DeliveryResult = {
  remoteId?: string
  url?: string
  state: "queued" | "draft" | "published" | "failed" | "unknown"
  rawStatus?: string
  error?: string
}

interface PublisherConnector {
  validate(input: DeliveryRequest): Promise<string[]>
  deliver(input: DeliveryRequest): Promise<DeliveryResult>
  inspect(remoteId: string): Promise<DeliveryResult>
}
Enter fullscreen mode Exit fullscreen mode

The unknown state matters more than it first appears. If a request times out after reaching the destination, the client cannot safely assume failure. Immediately sending the same request again may create a duplicate post. Marking the run as unknown gives the worker a chance to inspect the remote platform before deciding what to do next.

Keep credentials out of the content path

Publishing systems often fail security reviews because credentials are handled as ordinary workflow data. A token gets pasted into a prompt, copied into a screenshot, written to a log, or stored next to a generated article.

The content worker should receive a connector reference, not the secret itself. Resolve credentials only inside the adapter that needs them, and redact authorization headers before telemetry is written. OAuth refresh tokens, API keys, app passwords, and browser-derived sessions also deserve different rotation and expiry rules. Calling all of them “the token” makes operational mistakes much easier.

It also helps to request the smallest practical permission set. A workflow that only creates posts should not automatically receive account administration privileges. When a platform separates draft creation from public publishing, keep those permissions separate if your architecture allows it.

Browser-session connectors need special care. They can be useful when a platform has no suitable public API, but they should be treated as expiring credentials rather than permanent integrations. A health check before a scheduled run is far better than discovering a dead session halfway through a batch.

Separate creation from delivery

An article should become an immutable revision before it enters the publishing queue. This sounds fussy until an editor changes the title while a retry is in progress and two destinations receive different versions under the same job ID.

A practical record might contain the source revision, rendered Markdown, media references, destination policy, and a content hash. The delivery job then points to that snapshot. Any later edit produces a new revision rather than silently changing an in-flight request.

This separation also makes human review and unattended automation compatible. A team can require review for public posts while allowing approved revisions to move through the same delivery machinery. The pipeline does not need two completely different implementations.

Policy belongs here too. Before a job reaches a connector, check title length, tag limits, unsupported Markdown, image accessibility, and whether the destination permits the requested visibility. Validation errors should be returned as editorial feedback, not discovered after an avoidable API call.

Make retries boring

Retries are where a publishing demo becomes production software.

Generate a stable idempotency key from the destination, account, and immutable article revision. Store it before making the remote request. If the platform supports native idempotency headers, use them. If it does not, keep a local delivery ledger and search for an existing remote ID before creating another post.

Not every error should be retried. A temporary network failure, rate limit, or server error may justify exponential backoff with jitter. Invalid credentials should pause the connector and surface an actionable alert. Rejected content needs an editorial change. Repeating either of those requests every minute only creates noise.

A worker loop can stay simple:

const result = await connector.deliver(job.payload)

switch (result.state) {
  case "published":
  case "draft":
    await complete(job.id, result)
    break
  case "queued":
  case "unknown":
    await scheduleInspection(job.id, result.remoteId)
    break
  case "failed":
    await applyRetryPolicy(job.id, result.error)
    break
}
Enter fullscreen mode Exit fullscreen mode

Notice that submission and verification are separate actions. A remote service may accept a post and process it asynchronously, or the platform may moderate it before it becomes public. The final URL and observed visibility are better evidence of success than a 200 OK alone.

Observe outcomes, not just requests

Useful publishing telemetry answers ordinary operational questions. Which revision was sent? Which account and destination received it? Was it created as a draft or made public? What URL came back? How long did verification take? Was a retry safe, and why did it happen?

Keep a structured event trail without storing secrets or full sensitive payloads. A concise sequence such as validated, submitted, remote_id_received, visibility_checked, and published is much easier to investigate than a single generic “success” log.

This is also where automatic publishing benefits from a final read-back. Fetch the remote article after creation and compare important fields such as title, canonical URL, and visibility. Renderers may transform Markdown, strip unsupported HTML, or reject media even though the article itself was created.

The connector is not the product

Once the pipeline is reliable, swapping a destination adapter becomes routine. The valuable part is the system around it: immutable revisions, scoped authorization, explicit state transitions, safe retries, platform-aware validation, and verifiable outcomes.

That design also keeps AI in the right place. A model can research, draft, reframe, and adapt tone, but it should not be responsible for guessing whether a timed-out request created a public post. Deterministic code and persisted state are better tools for that job.

Content automation becomes genuinely useful when publishing is uneventful. No duplicate articles, no leaked keys, no mysterious “successful” jobs with missing URLs, and no need to manually inspect every destination after each run. The goal is not merely to send content faster. It is to make delivery predictable enough that the team can stop thinking about it.

Reference

Top comments (0)