draftaday docs draftaday.com Join the beta
On this page

Docs · page 06 of 09

Destinations, scheduling and publishing

Studio treats one post as a source and every place it goes as a separate publication, with its own body, its own outcome and its own link. This page covers connecting a destination, what each platform can actually carry, how a post moves from draft to published, and how to find out where it really went.

Connect a destination

Connections are per workspace. Publishing resolves strictly the post's own workspace token — there is deliberately no fallback to another workspace's connection.

To connect LinkedIn: open Settings → LinkedIn account and press Connect LinkedIn. You approve on LinkedIn's own consent screen and land back with a toast. The status pill on that card reads Not connected, Connected or Expired.

The small pill at the bottom of the sidebar is a different thing: it reports your Studio connection (whether this browser's token can reach Studio at all), not LinkedIn. For LinkedIn's status, look at the LinkedIn account card.

The requested scopes are openid profile email w_member_social — a member profile, nothing more. The one-time CSRF state that carries your workspace through the round trip lives for 10 minutes; a stale consent link comes back as Invalid or expired authorization (please retry).

To keep the connection alive: press Reconnect once. The callback stores the refresh_token LinkedIn returns, which is what lets Studio renew the connection unattended. A re-consent that returns no refresh token will not wipe a working one.

To connect Facebook Pages: open Compose, find the Facebook row in the destinations tray and press Connect…. (There is no Facebook card in Settings — Compose is the only way in.) Studio exchanges the code for a long-lived user token and then reads a Page token per Page you administer, storing every Page it can reach — not just the first. Scopes: pages_show_list, pages_manage_posts, pages_read_engagement, business_management, instagram_basic, instagram_content_publish.

Instagram rides on that same connection. An Instagram Professional account is reached through the token of the Page it is linked to; there is no separate Instagram login. If the Page has no linked account you get no Instagram account linked to <Page>; if the token predates the Instagram scopes, reconnect Facebook to grant Instagram access.

To connect TikTok: in the same Compose destinations tray, press Connect TikTok… (Reconnect TikTok… once a connection exists). Scopes are user.info.basic,video.upload. The access token lasts 24 hours and renews itself; the refresh token rotates and expires after a year, which is the one a human has to act on.

Good to know. A LinkedIn consent that returns no member identity is treated as a failed connection and never stored — a "connected" card that could not author a post would be a lie. LinkedIn Pages are still blocked: the Community Management scopes are unapproved, so a Page publish fails with this LinkedIn connection cannot post to a Company Page yet — it was authorised without w_organization_social.

What each platform can carry today

DestinationTextMediaLimit
LinkedIn memberyesimages (native multi-image up to 9), one video, or one document/PDF — never mixed3,000 characters, counted in Unicode code points and re-checked after hashtags are appended
Facebook Pageyesup to 10 images, or one video — images or a video, not both63,206 characters
Instagramcaption only — a post without media is refusedone video (Reels) or up to 10 images2,200-character caption
TikTokphotos carry a title and description; a video carries none — TikTok's editor owns that captionone video, or photosdraft only (see below)

LinkedIn also posts a first comment on the fresh share immediately after a successful publish. It is the first_comment the generator already wrote into the post's package — if there is none, nothing is posted. It is trimmed to 1,200 characters rather than refused, it is strictly best-effort (a comment failure is noted in the result detail and never changes the publish outcome), and it can be switched off server-side with LINKEDIN_FIRST_COMMENT=0.

LinkedIn renders real @mentions, but only where the post text actually spells the person's name — a tag the copy never spells is dropped. Compose warns before you schedule: "LinkedIn only renders a tag where the post spells the person's name."

Instagram publishes in two steps — a container is created, then polled until it reports FINISHED, then published. Publishing early fails, and a container that sits too long comes back as the media container expired before it was published.

TikTok does not publish. Studio uses MEDIA_UPLOAD, which drops the file into your TikTok inbox as a draft. The result is recorded as manual, not published, with a detail of inbox-draft (…) and a toast reading "Sent to <destination> as a DRAFT — open the app, tap the notification and post it. The caption is on your clipboard."

Careful with that last sentence. Nothing actually puts the caption on your clipboard — the publisher hands it back for the UI to offer, and the UI does not yet take it. For a video the caption is not sent to TikTok at all, so copy it out of the post in Compose before you switch to the app.

Post by hand

✋ Post by hand… in Compose automates everything up to the Post button. It offers eight targets, and they fall into two groups: destinations Studio genuinely cannot reach through an API, and destinations it publishes to automatically but that you may want to put up yourself.

TargetButtonLimitWhy it is on the sheet
LinkedIn PageCopy & open3,000LinkedIn has not granted the Community Management scopes yet
LinkedIn profileCopy & open3,000Studio publishes here automatically — this is the manual route
Facebook profileCopy & open63,206Meta removed publish_actions in 2018; no API posts to a personal timeline
Facebook PageCopy & open63,206Studio publishes here automatically — this is the manual route
InstagramCopy & open2,200blocked outright when the post has no media
WhatsAppOpen with the text65,536messaging, not publishing — there is no Status API
XOpen with the text280no X account is connected to Studio
ThreadsOpen with the text500no Threads account is connected to Studio

Where the platform has an intent URL (WhatsApp, X, Threads) the button reads Open with the text and the words are already in the composer. Everywhere else the button reads Copy & open and the clipboard is the only carrier — so a failed copy is reported, never assumed: Could not copy automatically — select the text in the box below and copy it by hand. The text is always in a box on the sheet as a fallback, with ⧉ Copy text beside it.

Media never travels through a URL, so the sheet offers each file as a download and says why: "Download the video first — you attach it in the composer." WhatsApp says it differently, because the file goes in the chat rather than a composer: "The link carries the words only. Download the video and send it in the same chat." Instagram is blocked outright with no media: Instagram will not accept a post without a picture or video. Add media first. Each row warns when your text is over that platform's limit. Tick Posted once you have done it.

The lifecycle, and what stays editable

draft → scheduled → publishing → published / failed / manual

Those six are the only statuses a post can hold. Edit text, hashtags, title, pictures, carousel and video freely while a post is a draft, scheduled, failed or manual — the scheduler reads the post as it stands at firing time, so an edit never needs re-scheduling. A post caught mid-flight refuses: cannot edit a post while it is publishing.

Compose is narrower in one respect: it will not reopen a published post for editing, and answers post is not editable (draft/failed/manual only). Read that message as "not editable in this status" — despite its wording, a scheduled post does save from Compose.

To take a post out of play: press 🗑 Trash. Trash is a freeze, not a status: the post stays on your calendar, the scheduler skips it, and any edit or move answers This post is in Trash — restore it before editing or moving it. Trashing a post that is mid-publish is refused: This post is being published right now — it cannot be trashed. Restore brings it straight back.

The Content Calendar

The grid offers Month, Week and List. A seven-chip legend — All, Draft, Scheduled, Publishing, Published, Manual, Failed — filters the grid and the list below it in sync, with platform chips beside it for the platforms actually present in the period shown. Busy days collapse at three events into "+N more"; hover a day to see everything on it.

To reschedule: drag a post to another day or time slot. Only draft, scheduled and manual posts are draggable — published, failed, publishing and trashed events stay put. The grid snaps to 5 minutes as you drag, the dropped time is rounded to the nearest 15 minutes, and the move is then confirmed in a Move "…" dialog whose field steps in 15-minute steps — nothing changes until you press 📅 Move it. A scheduled post cannot land in the past: That time is in the past — pick a future slot for a scheduled post. (Clicking an event instead opens its preview, with Move, Edit and Publish one click away inside.)

The world-clock bar rides on the end of the legend row: your local zone plus the zones you choose in Settings (up to 8; Edit zones jumps there). Zones on your calendar date show the 24-hour time; zones across the date line show their date too.

Scheduling and the auto-publisher

From Compose, press 📅 Schedule…, pick a local time and Confirm schedule. When Studio has a best-time suggestion that still lies in the future, a ✨ AI best time button appears beside the field and fills it with that slot — resolved to its next future occurrence — in one tap; with no suggestion the button is simply absent. Past times are always refused: Choose a time in the future.

If the content is an exact duplicate of something already published, or of a scheduled post due before this one, the card carries a non-blocking warning: Exact duplicate of published post … Not blocked — review before it posts.

The background publisher polls every 60 seconds, holds a 300-second lease on a claimed post, retries at most 3 times with backoff from 60s up to a 1,800s cap, and fires at most 25 posts per tick. Health is at GET /api/scheduler/status.

Publications: one post, N destinations

Each destination gets its own record, keyed by platform and account. Its body is resolved per field — a publication override wins, then the brand's body, then the post — and at send time Studio stores a snapshot of exactly what was submitted, along with the status, permalink and external id. That snapshot is the point: the post keeps being edited after it publishes, so without it "what went to Page B" would be whatever the post says today.

Publish guards

One share per destination, protected in layers: an atomic claim means only one worker can move a post into publishing; a second attempt gets post is already being published (409). The platform's share URN is persisted the instant it is accepted — before the terminal status is written — so a crash mid-publish reconciles instead of re-posting. A 2xx with no URN stores a published-no-urn sentinel so even that share cannot re-send, and a destination that already has the post is skipped with already-published (idempotent skip) rather than sent twice.

Failure states, and which ones retry

Permanent — fix them in Settings, then retry by hand:

  • LinkedIn is not connected — open Settings → Connect LinkedIn.
  • LinkedIn token expired — reconnect in Settings.
  • LinkedIn identity unknown — disconnect and reconnect in Settings.

These three answer 409 with a reconnect flag so the app can offer the one-click fix, rather than a 502 that reads as "the server is down". A per-destination validation failure (bad media selection for that target) is also permanent, and fails only that destination — the others still go.

Only genuinely pre-send failures back off and retry: a clean network blip, an in-process exception, or HTTP 408/429. Timeouts and 5xx are treated as permanent on purpose — the platform may have committed the share and there is no idempotency token, so a retry could double-post. And a post retries only if every failing destination on it is transient; one permanent failure makes the whole post permanent.

Reading the trail

Three records, in order of detail. Every publish attempt writes an audit row to the publish log — validation failures, per-platform results, permalinks and errors — which surfaces per post at GET /api/posts/:id/scorecard (the 50 most recent rows, under publish). The publications table holds one row per destination with its status, permalink, external id and submitted snapshot. And on the calendar, hovering a single destination badge shows that destination by name — the Page, or the @handle and the Page it hangs off — its state, its error if any, and What was submitted: the excerpt, character count, tag count and media that actually went out.