Workers

Publishing

Minute-level publisher. Scans an external content queue plus a workspace-scoped queue every minute, pulls due posts, fetches the video or image bytes from R2, posts via the unified-connector integration project, and persists publish events for the dashboard.

Updated today

Overview

At a glance
Worker
ultron-publish
Trigger
* * * * * (every minute)
D1
ultron-publish-db
R2
ultron-publish-assets (transient buffer)
Connector project
content_engine (siloed)
Output
One publish_events row per attempt, success or failure

The publisher is intentionally dumb. It does not pick what to post or when; that is the marketing swarm's job upstream. All it does is: at the right time, take a row, fetch its asset, push it to the channel via the connector, record the result.

Worker shape

wrangler.toml (key bindings)toml
1name = "ultron-publish"
2main = "src/index.ts"
3
4[triggers]
5crons = ["* * * * *"]
6
7[[d1_databases]]
8binding = "PUBLISH_DB"
9database_name = "ultron-publish-db"
10
11[[r2_buckets]]
12binding = "PUBLISH_ASSETS"
13bucket_name = "ultron-publish-assets"
14
15[vars]
16COMPOSIO_PROJECT_NAME = "content_engine"
17COMPOSIO_API_BASE = "https://backend.composio.dev/api/v3"

Queue sources

SourceHow it is consumed
Workspace content_queue tablePer-row status pulled to 'sending' under a row-level lock. Status advances through 'sent' or 'failed'.
External content repo (read-only poll)Every tick checks for new directories under content/youtube/queue/. Each new directory is enqueued as a row in content_queue.
Note
The external repo is treated as an append-only feed. The worker only reads; it never writes back. New directories surface as queued rows whose payload references the path; the worker then fetches the actual bytes when the row's due time arrives.

Publishing flow

publish flowtxt
1# Tick (every minute):
2# 1. Poll external repo for new content/<channel>/queue/ directories.
3# For each new one, insert a content_queue row.
4# 2. Read due rows: WHERE status='queued' AND due_at <= now()
5# Lock to 'sending' inside a transaction.
6# 3. For each locked row:
7# a. Fetch the asset bytes (video / image) into R2 if not cached.
8# b. Hand a public URL of the R2 object to the connector.
9# c. Call the connector to post on the target channel.
10# d. Persist a publish_events row (success or failure + error).
11# e. Update the queue row: status='sent' or status='failed'.
12# 4. Reset stale 'sending' rows older than 10 minutes to 'queued' (handles crash mid-tick).

Supported channels

ChannelNotes
YouTubeLong-form video uploads with title + description + tags. Supports unlisted scheduling via the connector.
InstagramReels + carousels via the connector's Instagram graph API surface. Captions support hashtag lists.
TikTokVideo upload via the connector. Caption with hashtags.
LinkedInNative post (text + media). Page-level posts only in v1.
XNative post (text + media). Threaded posts post each message sequentially.
RedditSubreddit post. Honours per-sub link / text constraints.

Retries and dead-lettering

Warning
A failed post moves to status='failed' after three attempts. The dashboard surfaces failed rows; the operator chooses to retry, edit, or discard. No automatic eternal retries - failed publishes often indicate a connector auth lapse that needs human attention.
ultron-publish-db.publish_eventssql
1CREATE TABLE publish_events (
2 id TEXT PRIMARY KEY,
3 queue_row_id TEXT NOT NULL REFERENCES content_queue(id),
4 attempt INTEGER NOT NULL,
5 status TEXT NOT NULL, -- 'ok' | 'error'
6 channel TEXT NOT NULL,
7 channel_post_id TEXT, -- the platform's post id when status='ok'
8 error_message TEXT,
9 created_at TEXT NOT NULL DEFAULT (datetime('now'))
10);

File map

cf-workers/ultron-publish
wrangler.toml
src
index.tsscheduled() loop
poll.tsexternal-feed polling
publish.tsper-row publish path
channels
youtube.ts
instagram.ts
tiktok.ts
linkedin.ts
x.ts
reddit.ts
migrations
0001_content_queue.sql
0002_publish_events.sql