Use this skill whenever Alex asks Hermes to manage organic social posts, drafts, media, schedules, approvals, connected accounts, or social publishing through the Hermes Social Console.
Hermes Social is a sister app to Hermes Ads, not a replacement. It should follow the same internal-dashboard/compliance posture:
Current planned/default target:
/home/avalon/apps/hermes-socialhttps://hermes-social.apps.poofc.comhttp://127.0.0.1:4029hermes-social/home/avalon/apps/hermes-social/data/hermes-social.sqlite/home/avalon/apps/hermes-social/uploadsEnvironment variables for scripts:
export HERMES_SOCIAL_URL="https://hermes-social.apps.poofc.com"
export HERMES_SOCIAL_API_KEY="...server-side key from uncommitted .env..."
Never print or paste HERMES_SOCIAL_API_KEY, OAuth tokens, app secrets, page tokens, or token table contents into chat.
Allowed by default:
Requires explicit approval:
Blocked unless Alex gives a very specific batch approval:
Important distinction:
approved_local.Operating rule: anything Alex can do in the Hermes Social UI should have a Hermes-agent usable CLI/API path documented here. Prefer the app's scripts or /api/hermes/... endpoints over browser UI automation. When new UI features are added, update this skill with the matching CLI command/API call in the same session.
When Hermes creates or presents a draft in Telegram, always include a direct Hermes Social UI link to the draft alongside the draft ID. Use ${HERMES_SOCIAL_URL}/?draft=<draft_id> unless the app exposes a more specific canonical draft route.
Run from the app directory unless using absolute paths:
cd /home/avalon/apps/hermes-social
HERMES_SOCIAL_URL="https://hermes-social.apps.poofc.com" \
HERMES_SOCIAL_API_KEY="$HERMES_SOCIAL_API_KEY" \
python3 scripts/social_accounts.py
Account UX/API rule: the Accounts tab must show platform-specific identities, not just platform + ID. For each connected Meta account, expose/render a structured identities array with:
avatar_url), display name, Page ID, and direct https://www.facebook.com/<page_id> link.instagram_avatar_url), handle when available (@instagram_username), Instagram Business ID, and direct https://www.instagram.com/<username>/ link.Persist Instagram profile context from Meta sync (instagram_business_account{id,username,profile_picture_url}) into server-side columns such as instagram_username and instagram_avatar_url; add lightweight SQLite migrations for new columns. If an existing account already has only instagram_business_account_id, refresh username/avatar via the server-side Meta/System User token and update the row. Never ask Alex to visually compare raw IDs when names, avatars, and direct links can be shown.
python3 scripts/social_create_draft.py \
--title "Weekly Astro Mage post" \
--caption "Caption text for Alex to approve." \
--hashtags '["AstroMage", "Astrology"]' \
--targets '["acct_..."]' \
--scheduled-for "2026-05-16T10:00" \
--variants '{"facebook":"Longer Page caption","instagram":"Short IG variant","tiktok":"Video hook"}'
This creates a local draft only. It does not publish.
python3 scripts/social_upload_media.py /absolute/path/to/image-or-video.mp4
Use the returned media.id in draft creation/editing. Platforms such as Instagram/TikTok often require publicly reachable media URLs. In production, uploaded media should go to Hetzner S3 bucket hermes-social and return https://hel1.your-objectstorage.com/hermes-social/...; if S3 is unavailable, the app should save locally under /uploads and return a warning instead of failing the whole upload.
python3 scripts/social_get_draft.py draft_...
Use this for agent-safe draft content/media replacement after uploading assets. This edits a local draft only; it does not publish.
curl -fsS -X PATCH "$HERMES_SOCIAL_URL/api/hermes/drafts/draft_..." \
-H "Authorization: Bearer $HERMES_SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"title": "Updated title",
"caption": "Updated caption",
"hashtags": ["Astrology"],
"media_asset_ids": ["media_..."],
"platform_variants": {"publish_target":"instagram","post_type":"carousel"}
}'
If /api/hermes/drafts/:id PATCH returns 404 on an older deployment, update/deploy Hermes Social; commit 9324f49 added the Hermes API alias for the existing browser draft edit route.
Settings opens on a mobile-first Connect social accounts surface with Facebook, Instagram, and Connect both actions. Keep the operator-heavy Meta Business/Page/App/System User wizard behind a collapsed Advanced setup disclosure. Anything in that UI must stay mirrored here as CLI/API capability.
Run a redacted readiness report without printing secrets:
cd /home/avalon/apps/hermes-social
python3 scripts/social_setup_check.py
This reports configured env keys by presence/length only, connected Page/IG account state, and readiness checks for Meta app credentials, Hermes CLI key, Page OAuth sync, Instagram target, and System User token.
Hermes-agent automations and scheduler scripts must use the /api/hermes/... route family with HERMES_SOCIAL_API_KEY, not the browser-admin /api/... routes that require an hs_session cookie.
For an explicitly approved draft:
curl -fsS -X POST "$HERMES_SOCIAL_URL/api/hermes/drafts/draft_.../publish-now" \
-H "Authorization: Bearer $HERMES_SOCIAL_API_KEY" \
-H "Content-Type: application/json" \
--data '{"platform":"instagram"}'
If a scheduler/CLI publish gets 401 Unauthorized, first check that it is using /api/hermes/drafts/:id/publish-now; /api/drafts/:id/publish-now is browser-admin and will fail without the login cookie.
Design rules:
<details> section..calendar-layout > .panel:last-child; once the inline panel is removed this can hide the only Calendar panel and make the tab appear blank. Prefer explicit overlay class selectors and verify the built CSS no longer contains accidental display:none rules for the main calendar panel.backdrop-filter) containing the draft title, source/date metadata, status chip, and Close button, plus a separate scroll body for the post preview. Hide duplicate in-card title/status rows inside the overlay so the preview starts with the post/media rather than repeated chrome.align-items:flex-end plus max-height:86dvh; this creates a huge blurred gap above the header on iPhone. For mobile, stretch the overlay/panel from the top safe area (align-items:stretch, height:100%, grid-template-rows:auto minmax(0,1fr)) so the blurred header sits near the top and only the preview body scrolls. Desktop can override back to centered with a bounded height.referrerPolicy="no-referrer" where applicable.Hermes Social intentionally uses a lightweight native Date calendar/agenda instead of Schedule-X. The calendar should be mobile-first with Day / Week / Month modes: default to Week for density, keep Month available as overview, and make tapping a Month day open the Day view so multiple scheduled drafts on that date are inspectable. In Month cells, keep count bubbles visually unobtrusive in the bottom-right; avoid crowding the top-right date number. iOS Safari/WebViews still lack the Temporal global, and Schedule-X bundles can crash mobile startup with Can't find variable: Temporal. If a calendar crash appears on iPhone:
Temporal, @schedule-x, @preact/signals, and preact.dist/ is not stale by rebuilding and checking the public index-*.js asset referenced by deployed HTML.events with id, title, start, end/status) so UI swaps do not disturb drafts/publishing.For S3 + calendar fix details, see references/hermes-social-s3-calendar-temporal.md.
For post-faithful Draft Queue/Calendar selected draft preview patterns, including media attachment shape and iOS Temporal polyfill notes, see references/hermes-social-post-preview-ui.md.
For interrupted self-service Instagram Business Login scaffolds, recover from the existing worktree and focused RED tests rather than redesigning: wire the connection service into routes, token/origin-aware publishing, readiness projection, and the minimal Connect UI; keep the operator wizard behind Advanced setup. See references/hermes-social-self-service-instagram-connect.md.
For Meta/Instagram Page-edge debugging, Creator account handling, preserving configured IG IDs, and PM2 env leakage pitfalls, see references/hermes-social-meta-instagram-access.md.
For the Postiz-vs-Hermes Meta publishing comparison, provider patterns, permalink fix, and future assurance checklist, see references/hermes-social-postiz-meta-assurance.md.
For the Marketing Core pattern that lets Hermes Social and Hermes Ads share Meta account setup without merging into one monolith, see references/hermes-marketing-core-social-ads-integration.md.
When Alex asks whether Hermes Social and Hermes Ads should merge, prefer a shared Marketing Core account graph over a full monolith. Social should initially host the shared graph because it already has encrypted Meta OAuth/token storage; Ads can later read the graph with its existing .env fallback until fully migrated. Keep Social organic publishing, Ads paid-media/spend, and Creative brand/media generation separate internally, but present account setup as a unified Marketing Core layer.
Model this like Meta Business Manager: organizations/business portfolios own many Pages, Instagram accounts, ad accounts, pixels, and projects; projects link to whichever assets they may use; module activations (social, ads, creative) control which Hermes app can use those assets. Never collapse this into a single flat account, because users may manage multiple brands, Pages, IG accounts, ad accounts, and pixels.
Do not expose token ciphertext or raw secrets in Marketing Core APIs. Summaries should return only counts, asset identities, non-secret statuses, and activation states. Treat Creative handoffs as local-draft-only and preserve existing Creative env contracts unless explicit compatibility migrations are added.
For the Ads integration phase, start with read-only awareness instead of credential migration: Ads may call Social's GET /api/hermes/marketing-core/summary server-side using HERMES_SOCIAL_API_KEY, expose a compact Ads-local /api/marketing-core/summary, and add marketing_core plus account_source to /api/dashboard. Keep Ads Meta API reads/writes on existing .env credentials as fallback (marketing_core_with_env_write_fallback vs env_fallback) until ad accounts/pixels are explicitly linked and activated. Normalize the Ads response to counts, activation strings, and non-secret primary Page/IG identities only — do not proxy raw Marketing Core rows or token metadata.
For product launches, create/select an explicit Marketing Core project/campaign object for the actual product being marketed rather than relying on a seeded/default project name. For Astral Hermes, wire Creative/Social/Ads around a project such as Astral Hermes with the public free-reading URL, offer, CTA, UTM conventions, and funnel events. See references/astral-hermes-launch-marketing-core.md for the concrete launch pattern.
Detailed implementation notes and verification checklist are in references/hermes-marketing-core-social-ads-integration.md.
MVP order/current status:
/api/oauth/meta/start redirects through Meta Login, /api/oauth/meta/callback validates state, exchanges the code, stores encrypted user/page tokens, and syncs Facebook Page accounts plus linked Instagram Business account IDs.
- The currently deployed legacy Facebook Login flow remains Page-only: pages_show_list, pages_read_engagement, pages_manage_posts. If adding instagram_basic or instagram_content_publish produces “Invalid Scopes,” treat that as a Meta app-product/configuration or access-review gap rather than proof that OAuth publishing is impossible; do not break the working Page flow while repairing it.
- For customer self-service Instagram publishing, prefer a separately configured Instagram Business Login flow requesting instagram_business_basic and instagram_business_content_publish. Exchange the authorization code server-side, verify /me?fields=user_id,username,account_type,profile_picture_url, accept only Business or Media_Creator, verify the publishing scope, exchange for a long-lived token, store it encrypted per account, and refresh it before expiry. A public username alone cannot reliably reveal account type; OAuth is the authoritative eligibility preflight.
- A System User token is an operator-managed legacy/enterprise credential, not something ordinary Meta OAuth can silently mint for an arbitrary customer. Keep it as a fallback for existing owned accounts, not the default self-service onboarding contract.
- Meta Page sync should request instagram_business_account{id,username,profile_picture_url} on /me/accounts when available and store the returned IG business ID on the Page account row. If Business Settings provides a verified IG Business ID, it may be stored as the server-side target after Alex explicitly provides it.
- If Alex says an Instagram account is “connected” in Business Settings but Hermes Social still shows no IG ID, do not assume app code is broken first: ask him to reconnect Meta OAuth in Hermes Social so /me/accounts is re-synced with the new Page↔Instagram link.
- Meta Business “Request Sent / access pending” for an Instagram account is approved in the owning Business portfolio’s Business Settings → Requests/Received/Instagram accounts, not necessarily in Instagram DMs.
- Alex’s current IG target: @druidmaxxing, Instagram Business ID 17841467108307246; it should appear as The Magi Page’s instagram_business_account after successful reconnect./{page_id}/feed, /{page_id}/photos, /{page_id}/videos) using the encrypted meta_page token. Mixed image+video Facebook posts are intentionally blocked; publish one video or one/more images./{ig-user-id}/media, bounded poll of /{creation-id}?fields=id,status_code,status until FINISHED, then /{ig-user-id}/media_publish). For a direct Instagram account, load the exact account and require Business/Creator type, publish_ready, the content-publish grant, and a valid encrypted account-specific token before any provider write. Carry that token's Graph origin through create/poll/publish/permalink calls. Never give an expired or missing direct-account credential an unrelated global System User token; legacy System User fallback is allowed only for accounts explicitly provisioned through the legacy Page-linked/System User flow. Normalize current documented OAuth/profile response envelopes (including data[0] forms), consume OAuth state on denial as well as success, enforce refresh eligibility, and keep public callback HTML free of unescaped provider/error text. Immediate publish after container creation can fail with Media ID is not available; use bounded polling before publish. After media_publish, fetch /{media-id}?fields=id,permalink and store the real permalink instead of constructing a URL from the media ID. A permalink lookup failure is metadata repair and must not trigger republishing. For CLI/API publishes, pass {"platform":"instagram","post_type":"story"} to force Stories, post_type:"reel" for Reels, or post_type:"carousel" for carousels; otherwise multiple media defaults to carousel and a single video defaults to Reel.Postiz remains a reference implementation for OAuth/media/platform quirks, not the final control plane. Do not create new Postiz drafts unless Alex explicitly asks to use Postiz.
references/hermes-social-mvp-deployment.md for the first MVP build/deployment reference: repo/path/PM2/port, SQLite/media paths, Schedule-X dependencies, Hermes operator scripts, nginx/certbot follow-up, and remaining Meta/Instagram/TikTok connector gaps.Before adding or operating any scheduled/background publisher, implement and verify an app-level Publish Safety Guard. Do not rely on Postiz/provider concurrency defaults as the safety layer; Postiz recognizes platform errors but its provider maxConcurrentJob values are high for Alex's use case.
Recommended conservative defaults for Alex's owned accounts:
/{ig-user-id}/content_publishing_limit before publish when available.spam, blocked, Page request limit reached, permission errors, invalid media). Retry only transient network/5xx once or twice with backoff.approved_local drafts may become scheduled/published, and every platform write should create a publish_attempt + audit row.For the detailed Postiz comparison, Meta docs excerpts, observed provider safeguards, and proposed Hermes Social guard schema/policy, see references/hermes-social-publish-safety-guards.md.
For embedded/unified-control-plane ports, including Caduceus Social/Ads migration evidence, target safety, partial-success handling, durable PAUSED Ads pushes, and minimum tests, see references/caduceus-social-ads-port-assurance.md.
For self-service Facebook/Instagram OAuth and account-readiness assurance—including current response envelopes, finite timestamp validation, exact credential and draft-target selection, disposable real-route --import provider spies, zero-write negatives, secret-free projections, migration rerun invariants, and concurrent publish claims—see references/meta-instagram-self-service-assurance.md.
Additional hard invariants for unified Social modules:
/me/accounts on current Graph versions should request Page tasks, not deprecated perms; perms can trigger (#100) Tried accessing nonexisting field (perms). Keep storage tolerant of legacy page.perms, but do not include perms in the fields query.