Skip to content

Scheduler

The scheduler manages all automation jobs across all connected phones. It runs as a daemon thread inside the Flask server, ticking every 30 seconds to enqueue, launch, monitor, and clean up jobs.

scheduled_jobs table (recurring configs)
|
| _scheduler_tick() every 30s
v
Step 0: Clean orphaned running jobs (dead PIDs)
v
Step 1: Enqueue due scheduled jobs -> job_queue (status='pending')
v
Step 2: Process each phone's queue
| - No running job? -> launch highest-priority pending
| - Running job finished? -> parse log, archive to job_runs
| - Higher-priority waiting >90s? -> preempt current
v
Step 3: Check running jobs for timeout
| SIGTERM -> wait 5s -> SIGKILL if still alive
v
Step 4: Detect externally finished processes -> archive
v
Step 5: Content plan pipeline tick
TypeScriptDefault TimeoutPurpose
crawlbots/tiktok/scraper.py900s (15min)Hashtag/user crawling
outreachbots/tiktok/outreach.py900sDM sending
postbots/tiktok/upload.py900sVideo upload
publish_draftbots/tiktok/upload.py900sPublish existing draft
perf_scanbots/tiktok/perf_scanner.py900sPost analytics scraping
inbox_scanbots/tiktok/inbox_scanner.py900sDM inbox scanning
content_genagent/agent_core.py3600s (1hr)LLM content planning
engagebots/tiktok/engage.py900sFYP engagement
skill_workflowskills/_run_skill.py900sRun a skill workflow
skill_actionskills/_run_skill.py900sRun a single action
app_exploreskills/auto_creator.py900sBFS app exploration
Terminal window
curl -X POST http://localhost:5055/api/schedules \
-H "Content-Type: application/json" \
-d '{
"name": "Daily cat crawl",
"job_type": "crawl",
"device": "L9AIB7603188953",
"interval_minutes": 1440,
"params": {"query": "#Cat", "tab": "top", "passes": 5},
"max_duration_minutes": 30,
"priority": 5
}'
  • Interval — run every N minutes (interval_minutes)
  • Daily times — run at specific times each day (daily_times: ["09:00", "18:00"])

Priority ranges from 1 (highest) to 5 (lowest). Higher-priority jobs can preempt lower-priority ones after the grace period.

When a higher-priority job is pending and the current job has been running for more than 90 seconds:

  1. Scheduler sends SIGTERM to the running process
  2. Waits 5 seconds
  3. Sends SIGKILL if still alive
  4. Launches the pending higher-priority job

Protected jobs: post and publish_draft are never preempted (interrupting would corrupt the upload state).

MethodEndpointPurpose
GET/api/schedulesList all scheduled jobs
POST/api/schedulesCreate a scheduled job
PUT/api/schedules/<id>Update a schedule
DELETE/api/schedules/<id>Delete a schedule
POST/api/schedules/<id>/toggleEnable/disable
POST/api/schedules/<id>/run-nowTrigger immediate run
GET/api/scheduler/statusPer-phone status (running job, PID, pending count)
GET/api/scheduler/queueCurrent job queue
GET/api/scheduler/queue/<id>/logsStream log lines
POST/api/scheduler/queue/<id>/killKill a job
POST/api/scheduler/runs/<id>/restartRe-enqueue a completed/failed job
GET/api/scheduler/historyArchived job runs
GET/api/scheduler/history/<id>/logsLogs for archived run
GET/api/scheduler/timeline24h timeline data
pending -> running -> completed | failed | timeout | killed | preempted
\-> archived to job_runs table
scheduled_jobs -- Recurring configs (name, type, phone, priority, interval/daily_times, params)
job_queue -- Active queue (pending/running with PID, log_file, timestamps)
job_runs -- Archived history (exit_code, duration, error_msg)

The Scheduler tab shows:

  • 24h Timeline — visual bars per phone, color-coded by job type
  • Schedules Panel — CRUD form for creating/editing schedules
  • Recent Runs — filterable table of completed jobs with status, duration, exit code
  • Queue Status — per-phone indicators showing running job and pending count

The Bot tab’s quick-launch buttons also enqueue to the same job queue.

ConstantValuePurpose
Tick interval30 secondsHow often scheduler checks all queues
_PREEMPT_GRACE_S90 secondsMinimum runtime before preemption
Default max_duration_s900 (15min)Per-job timeout
Kill escalation5 secondsSIGTERM -> SIGKILL delay
Terminal window
# Per-phone status
curl -s http://localhost:5055/api/scheduler/status | python3 -m json.tool
# Job queue
curl -s http://localhost:5055/api/scheduler/queue | python3 -m json.tool
# Job history
curl -s http://localhost:5055/api/scheduler/history?limit=20 | python3 -m json.tool

If a subprocess crashes without cleanup, the scheduler detects the dead PID on its next tick (30 seconds). You can also kill manually:

Terminal window
curl -X POST http://localhost:5055/api/scheduler/queue/<id>/kill

Check: does the schedule exist? Is it enabled? Is interval_minutes correct? Is another job already running on that phone?

The _phone_procs dict is in-memory and lost on restart. Orphaned jobs are cleaned up on the next tick via PID checking.