Publishing

How publishing works

The full publishing pipeline — from scheduled alarm to platform post, with retry and dead-letter handling.

When a content slot reaches its scheduled time, Vlozi runs it through a reliable, queue-backed publishing pipeline. This page explains exactly what happens from alarm to post.

The pipeline

scheduledAt arrives


SlotScheduler Durable Object alarm fires


fireSlot() — one queue message per target


Cloudflare Queue (content-publish-queue)

       ├──► runPublish() for each message
       │         │
       │         ├── Orphan check (slot/target/connection still exist?)
       │         ├── Terminal state guard (already published/failed?)
       │         ├── In-flight sentinel check (publishStartedAt set?)
       │         ├── Token refresh (Twitter only; LinkedIn/Meta lazy check)
       │         ├── Media URL resolution (via Media Service binding)
       │         ├── Pre-flight validation (char limits, scopes)
       │         ├── Set publishStartedAt (in-flight sentinel)
       │         ├── Platform API call (publish post)
       │         ├── Charge 1 credit (idempotent on targetId)
       │         └── Update target status + transition slot

       └──► Retry on failure (×3: +1m → +5m → +15m)

                 └──► Dead-letter queue (DLQ) after 3 failures

                           └── Mark target + slot as failed

Per-target isolation

A slot's targets are enqueued as separate queue messages. If a slot targets Twitter and LinkedIn:

  • Two messages go into the queue
  • They are processed independently
  • Twitter can succeed while LinkedIn fails and retries
  • The slot's overall status reflects all targets:
    • All published → published
    • Some failed after retries → partially_failed
    • All failed → failed

Retry policy

Attempt Delay before retry
1st retry 1 minute
2nd retry 5 minutes
3rd retry 15 minutes
After 3rd fail Moved to dead-letter queue

Retryable errors: rate limit (429), server error (5xx), network timeout.
Terminal errors (no retry): invalid token (401), permission denied (403), content policy violation.

The retry delay uses the platform's Retry-After header when provided. For Twitter rate limits, the actual reset time from x-rate-limit-reset is used.

At-least-once guarantee

The queue uses at-least-once delivery semantics. The engine guards against duplicate posts at each stage:

  1. Terminal state guard — if a target is already published or failed, the message is acknowledged and discarded
  2. In-flight sentinelpublishStartedAt is set before the platform API call. If the Worker restarts mid-call and a second delivery arrives, the sentinel marks the target as PUBLISH_INTERRUPTED and acks. This prevents double-posting when a crash happens after the first API call went out.

Credits

Credits are charged after a successful platform API call, not before. The charge is idempotent — if a retry is attempted after a successful publish that the Worker didn't acknowledge, the credit ledger returns the same transaction without double-charging.

If your credit balance is insufficient at charge time, the target is marked INSUFFICIENT_CREDITS and the message is acknowledged (not retried). The slot fails. Top up your credits and create a new slot.

Dead-letter queue

After 3 failed retries, the message moves to the dead-letter queue (content-publish-dlq). The DLQ handler:

  1. Marks the target as failed with reason DEAD_LETTER and the error detail
  2. Transitions the slot to failed (or partially_failed if some targets published)
  3. Acknowledges the DLQ message (no further retries)

NOTE

Credits for dead-lettered publishes are not automatically refunded. Contact support for manual reconciliation if this occurs.

What you can do after a failure

From the dashboard, open the failed slot and check the Activity log to see exactly which step failed and what error the platform returned.

Options:

  • Retry manually — reset the target to pending and re-enqueue via the activity log
  • Edit and reschedule — change the slot to failed → draft, fix the content, and re-submit
  • Delete — remove the slot if the content is no longer relevant

Publish logs

Every publish attempt is recorded in the publish_logs table. Each row captures:

  • Action: publish_attempt, publish_success, publish_failed, retry
  • Full platform response (JSON) for debugging
  • Error code and message

View them at Content Engine → [slot] → Activity or via the API:

GET /content/activity?from=2026-07-01T00:00:00Z&to=2026-07-15T00:00:00Z
Content Engine · PublishingEdit on GitHub