Scheduled Tasks
A cron-based scheduler polls every 30 seconds for due tasks. Each execution creates a conversation, copies MCP server configs, spawns an agent via the AgentExecutor, persists all events to the DB, broadcasts live SSE updates, and prunes old history.
Scheduler Loop scheduler.py asyncio.sleep(30) Check Due croniter evaluation next_run_at ≤ now Overlap Check policy: skip|allow check running execs Create Conv INSERT conversations + execution record Copy MCP task MCP servers → conv MCP servers Run Agent agent_executor.run_agent() async for event in ... DURING & AFTER EXECUTION SSE Broadcast _broadcast(conv_id, event) to /api/conversations/{id}/stream Persist Events INSERT messages, tool_calls flush text buffer on tool_use Update Stats UPDATE conversations SET tokens, cost, session_id, turn_usage Advance Cron _update_next_run() croniter.get_next() Prune _prune_executions() keep max_history (50) Execution record: UPDATE scheduled_task_executions SET status, finished_at, error_message, input_tokens, output_tokens, cost_usd, duration_ms _broadcast_done(conv_id) signals SSE subscribers stream is complete (sends None sentinel)

Configuration

  • cron_expr — Standard cron expression (croniter)
  • agent_type — claude / codex / gemini
  • model — Optional model override
  • timeout — Max execution time (default 600s)
  • overlap_policy — "skip" (default) or "allow"
  • max_history — Executions to keep (default 50)
  • Per-task MCP server config via scheduled_task_mcp_servers

Files Involved

  • web/scheduler.py — Scheduler loop, execution, broadcast
  • web/agent_executor.py — run_agent() subprocess
  • web/api/schedules.py — CRUD API routes
  • web/api/conversations.py — stream_conversation() SSE
  • web/database.py — scheduled_tasks + executions tables
  • web/models.py — ScheduledTaskCreate/Out/Detail