Schedulala
Audience: the agent. A user has pointed you at this file ("read https://schedulala.com/SKILL.md and follow it"). Install and connect Schedulala, then use it to schedule and publish their social posts. Sections 1–3 get you connected; sections 4–9 are how to work once you are.
Schedulala schedules and publishes social media posts across 12 platforms, with analytics, engagement (comments/replies), and social listening. Full REST API, CLI, and connector docs: https://schedulala.com/developers/docs — link users there; do not restate the REST reference.
1. Get access
The fastest path on any agent with a shell is the CLI (zero MCP tool-schema cost). Node >= 18, no other dependencies:
npm i -g schedulala # or prefix every command with: npx -y schedulala
The user gave you an API key (sk_live_ or sk_test_):
export SCHEDULALA_API_KEY=sk_live_... # or: schedulala config set api_key sk_live_...
schedulala whoami --json # exit 0 = ready · exit 2 = missing/rejected key
Key resolution order: --api-key flag > SCHEDULALA_API_KEY env >
~/.schedulala/config.json. Keys are minted in the dashboard at
https://schedulala.com/dashboard?view=developer (live or test). Pointing at
a non-production server (self-hosted, staging): --base-url <url> or
SCHEDULALA_API_URL env.
No key yet — run the device flow. Be honest with the user about the one human step it needs:
schedulala init --email their@email.com --json
This prints a code (like SCHD-XXXX-XXXX) and the URL
https://schedulala.com/developers/verify. The USER must open that URL in a
browser, sign in via magic link with the SAME email, and enter the code —
you cannot complete this step for them. The CLI then polls (up to 15
minutes; codes expire in 15) and saves the key to ~/.schedulala/config.json.
With --json, init emits TWO JSON documents on stdout (first the code +
URL, later the completion) — parse them as a stream, never JSON.parse the
whole output. Never ask the user to paste credentials into the chat.
What a fresh key gets (free taster): 2 connected accounts, 4 posts
total — lifetime, never resets, 30 requests/min, no posting to X/Twitter
(returns 402; connecting X still works). Existing Schedulala subscribers
get their paid plan on the same key automatically. Upgrade link:
schedulala upgrade --json. Test keys (sk_test_) simulate posts —
nothing publishes, no quota burn — ideal for a dry run. Note: sandbox posts
resolve IMMEDIATELY as posted with a sandbox id even when scheduled for
later; that's the simulation completing, not your schedule being ignored.
2. Identify which agent you are
| You are | Do this |
|---|---|
| Claude Code | claude mcp add --transport stdio --env SCHEDULALA_API_KEY=sk_... schedulala -- npx -y @schedulala/mcp-server — or skip MCP and use the CLI directly |
| claude.ai / Claude Desktop | Hosted connector (no key handling): Settings → Connectors → Add custom connector → https://schedulala.com/api/mcp (OAuth sign-in; paid Claude plan required). Adds interactive widgets: post previews, analytics cards, calendar, inbox, in-chat media uploader |
| OpenClaw | openclaw mcp add schedulala --url https://schedulala.com/api/mcp --transport streamable-http --header "Authorization: Bearer sk_..." — or stay CLI-only for zero tool-schema tokens |
| Cursor / any other MCP client | Remote: streamable-http https://schedulala.com/api/mcp with an Authorization: Bearer sk_... header. Local stdio: npx -y @schedulala/mcp-server with env SCHEDULALA_API_KEY (env ONLY — it does not read the CLI config file) |
| Anything else with a shell | The schedulala CLI alone does everything: --json on every command, exit codes 0–7. See references/cli.md |
Token-sensitive runtimes: the full MCP surface is ~16k tokens of tool
schemas, and the 9 show_* widget tools plus the attach_media widget path
only render on claude.ai/ChatGPT. Filter client-side (OpenClaw
toolFilter.include) to the core set — or use the CLI, which costs nothing.
3. Connect social accounts
Check what's connected first: schedulala accounts --json (MCP:
list_accounts). To connect more:
- Scriptable, no browser:
schedulala connect bluesky --identifier handle.bsky.social --app-password xxxx-xxxx-xxxx-xxxxandschedulala connect telegram --bot-token 123:ABC. - OAuth platforms (twitter, instagram, facebook, linkedin, tiktok,
threads, youtube, pinterest, google-business):
schedulala connect <platform> --jsonreturns an authorization URL — hand it to the user to open and approve; never try to complete OAuth yourself. - Mastodon: dashboard-only (per-instance OAuth) — send the user to https://schedulala.com/dashboard?connect=1.
Verify afterwards with schedulala accounts --json, then you're ready to
post (section 5).
4. Auth and safety rails
- Check auth before anything else:
list_accounts(MCP) orschedulala whoami(CLI). No accounts / auth error → section 1 / section 3. - OAuth, credentials, and payment happen ONLY on schedulala.com in the
user's browser — never in the chat. Never ask for passwords, app
passwords, bot tokens, or card details.
connect_accountandget_upgrade_linkreturn links; that is the entire flow. - Always confirm with the user before anything publishes (
publishNow, a near-termscheduledFor,reply_to_comment,hide_comment). Prefer drafts and previews first. - API keys:
sk_live_= real posting;sk_test_= sandbox (simulated, no real posting, no quota burn). CLI:--sandboxrequires ansk_test_key.
5. Core publishing workflow
- Discover —
list_accounts: platforms, usernames,accountIds, health.isConnected: false+needsReconnect: true= dead token → tell the user to reconnect in the dashboard; do not post to it. When MORE THAN ONE account is connected on a platform,accountIdis REQUIRED in the platforms array (the API returns a 400 listing the choices rather than silently picking). CLI:--platforms "bluesky=did:plc:xyz,twitter"—platform=accountIdwith the id fromschedulala accounts --json. (REST equivalent:POST /api/v1/postswithplatforms: [{platform, accountId}]— full reference at https://schedulala.com/developers/docs, don't guess other fields.) - Draft — write the content; per-platform text via the object form
{ platform, accountId, content }orplatformSettings.<platform>.content. - Validate / preview —
validate_postchecks limits, media counts, and required fields per platform WITHOUT posting. On the hosted connector, prefershow_post_preview: a read-only, faithful per-platform mockup (threads, image grids, carousels, PDF docs, 9:16 reels, polls, live char counts). Pass it the exact draft you'd givecreate_post. (The CLI has no validate command — validate via MCP/REST, or create with--draftfirst and inspect.) - Confirm — show the user what will go out, where, and when.
- Create —
create_postwith exactly ONE scheduling mode:scheduledFor(ISO 8601, 5 minutes to 90 days out) /publishNow: true/queue: true(auto-schedule next open slot) /status: "draft". CLI:schedulala post "<content>" --platforms <list>with--schedule <ISO8601>/--now/--queue/--draft(NO flag = draft) — full flags in references/cli.md. - Verify —
get_postfor per-platform status, URLs, and errors;retry_postre-runs failed platforms (optionally a subset, optionaldelayseconds). CLI:schedulala post:status <id> --watch.
Editing: update_post works on drafts and scheduled posts only (send only
changed fields; mediaItems replaces ALL media). cancel_post deletes drafts
and cancels scheduled posts; already-processing/posted posts can't be cancelled.
Bulk: bulk_create_posts takes up to 25 create_post-shaped entries.
Threads (chains): create_thread, 2–25 entries, on twitter / threads /
bluesky / mastodon only, max 1 media item per entry; the whole chain publishes
from ONE postId — get_post that id for status.
First comment (auto-plug): pass firstComment (plain string) or
firstComments ([{target, text}]) on create_post / bulk_create_posts /
create_thread to auto-post a comment ~15 seconds after publishing — the
"link in the first comment" workflow. Supported on linkedin, facebook,
instagram, youtube, twitter, threads, bluesky, mastodon (NOT tiktok /
pinterest / telegram / google-business — no comment API). Text and links
only. Comment limits differ from post limits — see
references/platforms.md. A failed comment never
fails the post; get_post reports delivery per platform.
Quota: threads and bulk consume post quota PER ENTRY (a 5-entry thread = 5
posts). Validation and account-resolution failures don't burn quota. Check
get_usage (or schedulala usage) when planning volume; on quota_exceeded
or the sandbox plan, hand the user the link from get_upgrade_link.
6. Media decision tree
Full protocol and limits: references/media.md
- User's file (on device or attached in chat) →
attach_media: on widget-capable clients (claude.ai) it opens an in-chat drop zone; on clients without widgets it still works — its result carries a programmatic presign→PUT→confirm upload API (see references/media.md). Full quality, images ≤20MB, videos ≤500MB, PDFs ≤100MB. NEVER read and base64-encode a user file yourself — chat channels truncate large payloads and destroy quality. - Public http(s) URL →
upload_media(modeurl): re-hosts on the CDN (images ≤20MB jpeg/png/webp, gif ≤15MB; videos ≤50MB mp4/mov/webm). Videos LARGER than 50MB: skip upload_media and pass the public URL directly inmediaItems— it is fetched at publish time. Re-hosted media expires after 30 days if unused. - Image YOU generated in this conversation (chart, card) →
begin_generated_media_upload→append_generated_media_chunk(sequentialseqfrom 0, ≤8000 base64 chars per chunk; 409 → resume fromexpectedSeq; 429 → wait, resend same seq) →finish_generated_media_upload. Images only, ≤3MB decoded, session expires in 30 minutes. - Already uploaded ("my latest upload", "that reel from earlier") →
list_media, then reference the returned url. - CLI:
--media <url[,url…]>(type inferred from extension).
Multiple mediaItems = carousel (instagram, facebook, threads, tiktok,
pinterest; mastodon up to 4 images). PDF (type: "pdf") = LinkedIn document
posts only.
7. Platform rules (the gotchas that reject posts)
Full per-platform schemas: references/platforms.md
| Platform | Must know |
|---|---|
| tiktok | platformSettings.tiktok.privacy (e.g. PUBLIC_TO_EVERYONE, SELF_ONLY) AND platformSettings.tiktok.postMode (direct = publish to profile, inbox = user finishes in the TikTok app) are BOTH required. No default — ASK the user if they haven't said. |
At least one media item required. Reel: platformSettings.instagram.postType: "reel". |
|
| youtube | Video required; platformSettings.youtube.title (≤100 chars) + description (≤5000). |
platformSettings.pinterest.boardName (+ 100-char title / 500-char description, optional link). |
|
PDF document post: mediaItem type: "pdf" + platformSettings.linkedin.documentTitle. |
|
| google-business | Exactly ONE image, no video; postType STANDARD / EVENT / OFFER with CTA buttons via platformSettings; 1500-char limit. |
| mastodon | platformSettings.mastodon.visibility (e.g. public, unlisted). |
| telegram | Polls via platformSettings.telegram.pollData. |
Character limits: twitter 280 · bluesky 300 · threads 500 · mastodon 500 · google-business 1500 · instagram 2200 · tiktok 2200 · linkedin 3000 · telegram 4096 · facebook 63206. (YouTube and Pinterest count title/description via platformSettings instead.)
8. Analytics, engagement, listening
On the hosted connector prefer the widgets: show_analytics (one account's
growth + top posts), show_leaderboard (ranked posts, sort
engagement|likes|impressions|date), show_comparison (side-by-side 2–5
entities — YOU supply the metrics from the analytics tools), show_calendar
(upcoming + recent posts), show_inbox (comments with Reply/Hide actions),
show_feeds (keyword listening feed), show_connections (accounts + plan).
Data tools (all interfaces):
get_post_analytics— per-post metrics + engagement rate. Platforms: instagram, twitter, bluesky, threads, facebook, linkedin, pinterest, tiktok, youtube, mastodon (NOT telegram / google-business). OmitaccountIdto merge top posts across all accounts (merged mode doesn't paginate).get_follower_growth— current count + 7d/30d growth + daily history. tiktok reports no follower counts; merged totals exclude YouTube.get_best_posting_times— top hours/days from engagement history.get_video_transcript/get_video_retention— YouTube only; retention has a 7-day cache and ~48h lag. Great for repurposing videos into posts.- Engagement — comments + replies on facebook, instagram, youtube, linkedin,
threads, bluesky, google-business; hide on facebook, instagram, youtube,
threads; mentions on instagram, threads, bluesky. Reply targets differ per
platform: instagram/youtube take
commentId; facebook/linkedin takecommentIdorpostId; threads takes either; bluesky requires all four replyRefs (parentUri,parentCid,rootUri,rootCid) from a comment'splatformData.replyRefs. google-business "comments" are customer reviews — one owner reply, ≤4096 chars, replying again overwrites.reply_to_commentandhide_commentPUBLISH changes — confirm first. - Listening —
search_feedssearches public posts on bluesky + threads (threads has a Meta budget of 500 searches per rolling 7 days per account, shared with the dashboard — prefer bluesky for exploration);lookup_profile(bluesky only);list_feed_keywords/update_feed_keywords(max 20 per platform).
9. Errors → what to do
| Symptom | Do |
|---|---|
| 400 "Multiple accounts…" | Re-call with the object form + accountId from the listed choices. |
quota_exceeded / sandbox plan ("posting disabled") |
get_upgrade_link → give the user the link. Reads still work on sandbox. |
Platform in failed state on get_post |
Read the per-platform error, fix (often a missing platformSettings field), retry_post. |
Account needsReconnect: true |
User reconnects in the dashboard (connect_account link); don't post to it. |
| Validation rejection | validate_post the fixed draft before retrying; check the table in §7. |
| CLI exit code 2 | Auth: run schedulala init or set SCHEDULALA_API_KEY. |
| 401 with a named error_description | "Missing or invalid Authorization header" = header shape; "Invalid API key format" = not an sk_ key; "Invalid API key" = wrong or revoked — mint a fresh one (section 1). |
Worked examples (create, tiktok, multi-account, thread, bulk): examples/ in the installed skill, or https://github.com/schedulala/schedulala-agent/tree/main/examples when you fetched this file over HTTP.
