Marketing Jobs Seam
β New in 1.3 β A single, deliberately narrow endpoint: POST /api/marketing-jobs/enqueue. Itβs how an outside agent hands Ghost a video to post, without reaching into anything else.
Why this exists β the split
Section titled βWhy this exists β the splitβGhostβs job is phone hands: driving a real Android device over ADB. Deciding what to post, when, and with what caption is a different job β a content brain β and it lives in a separate process (for the reference deployment, thatβs an external social-media-agent orchestrator).
The two talk through exactly one seam:
βββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ External agent (content brain) β β Ghost (phone hands) ββ β HTTP β ββ β’ Decides what/when to post β βββββββΆ β POST /api/marketing-jobs/ ββ β’ Writes caption + hashtags β POST β enqueue ββ β’ Renders the video β β β ββ β β βΌ ββ β β job_queue (status=pending) ββββββββββββββββββββββββββββββββββββ β β β β βΌ next scheduler tick β β bots/tiktok/upload.py worker β β β β β βΌ β β Saves as DRAFT on the phone β βββββββββββββββββββββββββββββββββββThe endpoint lives in the public gitd/ namespace (gitd/routers/marketing_jobs.py) so an external caller can drive Ghost without depending on the premium plugin.
The one rule: draft-only
Section titled βThe one rule: draft-onlyβThis is the whole safety model, and it is not configurable.
No matter what you send, the job is saved as a draft. The seam has no permission to live-publish.
In enqueue_marketing_job(), the configβs action is hard-overridden to "draft":
config = { "video": req.video_path, "caption": req.caption, "hashtags": req.hashtags, "action": "draft", # β forced, ignores req.action}If a caller explicitly passes action="publish", the request still succeeds β but the attempt is logged as a warning and the job is saved as a draft anyway:
if req.action and req.action.lower() != "draft": logger.warning( "marketing_jobs.enqueue: rejecting action=%r from external caller; " "saving as draft instead", req.action, )Going from draft β published is a separate, human-in-the-loop step. Live auto-publishing is intentionally not something an external agent can reach through this API.
Request
Section titled βRequestβPOST /api/marketing-jobs/enqueue
| Field | Type | Required | Notes |
|---|---|---|---|
video_path | string | β | Absolute path to the video on the machine running Ghost. Validated to exist. |
phone_serial | string | β | ADB serial of the phone to post from. |
caption | string | Post caption. Default "". | |
hashtags | string | Hashtag string. Default "". | |
tts_text | string | If set, Ghost injects a text-to-speech voiceover into the video. | |
account | string | Expected active TikTok account on the phone (sanity check). | |
scheduled_at | string (ISO) | Informational only β the job runs ASAP on the next scheduler tick, not at this time. | |
action | string | Ignored. Forced to "draft". |
Validation is strict up front (gitd/routers/marketing_jobs.py):
video_pathmust be absolute β400otherwisevideo_pathmust exist on disk β400otherwisephone_serialmust be non-empty β400otherwise
Response
Section titled βResponseβ{ "job_id": "ghost-job-42", "estimated_post_at": "2026-07-04T18:00:00Z", "action": "draft", "phone_serial": "YOUR_DEVICE_SERIAL"}estimated_post_at echoes back your scheduled_at β it is not enforced. job_id is the queue id you can watch in the Scheduler view.
Under the hood the endpoint wraps _enqueue_job(...) with job_type="post", priority=2, trigger="marketing_agent", and a max_duration_s of 1800. The existing bots/tiktok/upload.py worker picks it up on the next scheduler tick.
Example call
Section titled βExample callβcurl -X POST http://localhost:5055/api/marketing-jobs/enqueue \ -H "Content-Type: application/json" \ -d '{ "video_path": "/home/me/renders/clip_042.mp4", "phone_serial": "YOUR_DEVICE_SERIAL", "caption": "Ghosts in the machine π»", "hashtags": "#android #automation", "tts_text": "Meet Ghost, the open-source Android agent." }'Ghost queues the draft, the worker opens TikTok on the phone, uploads the video, fills the caption, and stops at the draft screen.
When to use
Section titled βWhen to useβUse the seam when:
- You have an external orchestrator that decides content and just needs Ghost to put it on a phone
- You want a hard guarantee that automation can only ever draft, never publish
- Youβre integrating Ghost as the βdevice layerβ under your own content pipeline
Donβt use the seam when:
- You want to drive the phone interactively β use Agent Chat or the MCP Server instead
- You need to publish automatically β by design, you canβt; publishing stays manual
- The video isnβt already rendered to a local file β this seam takes a finished file path, not a render request
Related
Section titled βRelatedβ- Scheduler β the job queue this endpoint feeds; watch
ghost-job-*runs here - MCP Server β the richer, tool-based way external agents drive Ghost
- ADB Device Control β the phone-hands layer that actually performs the upload