Skip to main content
Hayon’s scheduling system is built on RabbitMQ. When you schedule a post, a job is placed in the queue with a delivery time. A dedicated worker process picks it up and calls each platform’s API independently, so a failure on one platform never blocks the others.

How scheduling works

1

Set a future publish time

In the post editor, toggle Schedule for later and pick a date and time. Set your timezone so the time is interpreted correctly — the timezone field accepts any IANA timezone string (e.g. America/New_York, Europe/London).
2

Post is enqueued

On save, the backend sets status to SCHEDULED and stores the scheduledAt timestamp. It then calls Producer.queueSocialPost() for each selected platform, putting a PostQueueMessage into RabbitMQ with the target delivery time.
3

Worker processes the job

The PostWorker consumes messages from the queue. When the scheduled time arrives, it validates platform credentials, uploads any media, and calls the platform API. Each platform job runs independently.
4

Status is updated

After delivery, each entry in platformStatuses is updated to completed (or failed). The overall status field is set to COMPLETED, PARTIAL_SUCCESS, or FAILED depending on results.

Queue architecture

Each platform gets its own message in the queue. The PostQueueMessage payload carries everything the worker needs:
PostQueueMessage shape
{
  postId: string;       // MongoDB ObjectId
  userId: string;       // Owning user
  platform: PlatformType; // "bluesky" | "threads" | "instagram" | ...
  content: {
    text: string;
    mediaUrls: string[];  // Public S3 URLs
  };
  scheduledAt?: Date;   // Undefined = deliver immediately
}
Each postId + platform pair is treated as an independent job. If you publish to three platforms, three separate queue messages are created and processed concurrently.

Setting a scheduled time via API

Include scheduledAt (ISO 8601) and timezone in the create or update request body:
POST /api/posts
curl -X POST https://api.yourhayon.com/api/posts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "text": "Posting at peak time for maximum reach.",
      "mediaItems": []
    },
    "selectedPlatforms": ["bluesky", "mastodon"],
    "scheduledAt": "2026-05-15T09:00:00.000Z",
    "timezone": "Europe/Paris",
    "status": "SCHEDULED"
  }'
Response:
201 response
{
  "data": {
    "postId": "664f1a2b3c4d5e6f7a8b9c0d",
    "status": "SCHEDULED",
    "scheduledAt": "2026-05-15T07:00:00.000Z"
  }
}
The scheduledAt value in the response is stored in UTC regardless of the timezone you supplied.

Editing a scheduled post

Send a PUT request to /api/posts/:postId with the updated fields. The same schema used for creation is applied for validation. You can change the text, platforms, media, or reschedule to a new time.
PUT /api/posts/:postId
curl -X PUT https://api.yourhayon.com/api/posts/664f1a2b3c4d5e6f7a8b9c0d \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": {
      "text": "Updated caption before it goes out.",
      "mediaItems": []
    },
    "selectedPlatforms": ["bluesky"],
    "scheduledAt": "2026-05-15T11:00:00.000Z",
    "timezone": "Europe/Paris",
    "status": "SCHEDULED"
  }'
Editing a post re-creates its platformStatuses array and re-enqueues all platform jobs. If the previous messages were already consumed by the worker, the duplicate-detection check (based on platformStatus.status === "completed") prevents double-posting.

Cancelling a scheduled post

Send POST /api/posts/:postId/cancel to cancel a post that has not yet been processed.
POST /api/posts/:postId/cancel
curl -X POST https://api.yourhayon.com/api/posts/664f1a2b3c4d5e6f7a8b9c0d/cancel \
  -H "Authorization: Bearer <token>"
Successful response:
200 response
{
  "data": {
    "postId": "664f1a2b3c4d5e6f7a8b9c0d",
    "status": "CANCELLED"
  }
}
The status field is updated to CANCELLED in the database. When the worker picks up the queued messages for this post, it detects post.status === "CANCELLED" and skips them immediately without calling any platform API.
Cancellation works by marking the database record. The messages already in RabbitMQ are not physically removed — the worker is responsible for checking post status before processing.
Cancellation is only valid for posts in a cancellable state (i.e. not yet COMPLETED or already CANCELLED). Attempting to cancel a post that does not meet this condition returns a 404 error.

Retrying a failed post

If one or more platforms fail, you can retry by calling POST /api/posts/:postId/retry. Only platforms with platformStatus.status === "failed" are re-enqueued.
POST /api/posts/:postId/retry
curl -X POST https://api.yourhayon.com/api/posts/664f1a2b3c4d5e6f7a8b9c0d/retry \
  -H "Authorization: Bearer <token>"
Response:
200 response
{
  "data": {
    "postId": "664f1a2b3c4d5e6f7a8b9c0d",
    "retryingPlatforms": ["instagram"]
  }
}
Before re-enqueueing, the backend resets each failed platform’s status back to pending in the database:
Retry logic in post.controller.ts
await postRepository.updatePlatformStatus(postId, platform, {
  status: "pending",
  error: undefined,
});

await Producer.queueSocialPost({
  postId,
  userId,
  platform,
  content: { text, mediaUrls },
  scheduledAt: undefined, // Retries go out immediately
});
Retried posts are delivered immediately regardless of the original scheduledAt. They skip the scheduler and go straight to the front of the queue.

Automatic retries for transient errors

The PostWorker also handles retries internally for transient failures (network timeouts, rate limit responses). It checks whether an error is retryable and re-queues via the dead-letter exchange (DLX) with exponential backoff — up to 3 attempts per platform per job.
Retry logic in posting.worker.ts
const isRetryableError = (error: any): boolean => {
  if (error.rateLimited) return true;
  if (error.retryAfter) return true;
  if (
    error.message?.includes("timeout") ||
    error.message?.includes("ECONNRESET") ||
    error.message?.includes("ENOTFOUND")
  ) return true;
  return false;
};

const shouldRetry = attempts < 3 && isRetryableError(error);
After 3 failed attempts, the platform status is permanently set to failed and the message is acknowledged.

Post status transitions

FromToTrigger
DRAFTPENDINGUser publishes the draft.
DRAFTSCHEDULEDUser schedules the draft.
PENDINGPROCESSINGWorker begins processing.
SCHEDULEDPROCESSINGScheduled time arrives, worker begins.
PROCESSINGCOMPLETEDAll platforms succeed.
PROCESSINGPARTIAL_SUCCESSSome platforms succeed.
PROCESSINGFAILEDAll platforms fail.
PENDINGCANCELLEDUser cancels before worker picks it up.
SCHEDULEDCANCELLEDUser cancels before the scheduled time.
FAILEDPENDINGUser triggers a retry.

The calendar view

The Calendar page at /calendar displays all your scheduled posts on a calendar interface, letting you see posting frequency and manage upcoming content at a glance.

Build docs developers (and LLMs) love