Implementing Scheduled Tasks in AionUi: A Complete Guide to the Cron Subsystem

AionUi implements scheduled tasks through a three-layer cron subsystem comprising SQLite persistence in CronStore.ts, timer management and execution logic in CronService.ts, and React frontend hooks that synchronize job state via IPC.

AionUi ships with a full-featured cron subsystem that enables automatic message scheduling within conversations. Whether you need daily briefings, hourly updates, or one-time reminders, the implementation leverages native timers, SQLite persistence, and Electron's power-save blockers to ensure reliable execution. This guide examines the architecture, key source files, and practical implementation patterns for building scheduled tasks in AionUi.

Architecture Overview

The scheduled task implementation splits responsibilities across four logical layers:

Layer Responsibility Key Source Files
Persistence SQLite storage and CRUD operations for job definitions src/process/services/cron/CronStore.ts
Engine Timer creation, job execution, retry logic, and system resume handling src/process/services/cron/CronService.ts
Busy-Guard Prevents concurrent execution when conversations are processing messages src/process/services/cron/CronBusyGuard.ts
Frontend React components and hooks for job management via IPC src/renderer/pages/cron/components/CronJobManager.tsx, useCronJobs.ts

Persistence Layer: CronStore

The CronStore class in src/process/services/cron/CronStore.ts handles all database interactions for scheduled tasks. It defines the CronJob data model (lines 18-43) and provides bidirectional serialization through jobToRow and rowToJob functions.

Key capabilities include:

  • CRUD Operations: insert(), update(), delete(), listAll(), listByConversation(), listEnabled(), and deleteByConversation()
  • Singleton Pattern: Exported as cronStore singleton for application-wide access
  • Schema: Jobs store schedule expressions, next run timestamps, retry counts, and enabled status in the cron_jobs table

Scheduling Engine: CronService

CronService.ts in src/process/services/cron/CronService.ts implements the core scheduling logic, managing native timers and job lifecycle.

Initialization and Lifecycle

The init() method (lines 45-63) loads all enabled jobs from cronStore.listEnabled() and activates their timers. It also registers system resume listeners to handle missed executions after sleep.

Job Creation and Timer Management

The addJob() method (lines 70-126) persists new jobs and creates appropriate timers based on schedule.kind:

  • cron: Uses Cron object for standard cron expressions
  • every: Uses setInterval for recurring intervals
  • at: Uses setTimeout for one-time scheduled execution

Each timer updates nextRunAtMs in the database and emits ipcBridge.cron.onJobUpdated to synchronize the frontend.

Execution and Retry Logic

The executeJob() method (lines 90-140) checks cronBusyGuard.isProcessing() before firing. If the conversation is busy, it increments retryCount and schedules a 30-second retry. Successful executions update the conversation's modifyTime to trigger UI reordering.

System Resume and Power Management

handleSystemResume() (lines 55-92) detects missed jobs after system sleep, inserts warning messages via insertMissedJobMessage, and restarts timers. updatePowerBlocker() (lines 32-51) uses Electron's powerSaveBlocker.start('prevent-app-suspension') to keep the app alive while jobs are active.

Concurrency Control: CronBusyGuard

CronBusyGuard.ts in src/process/services/cron/CronBusyGuard.ts prevents race conditions by tracking per-conversation processing flags. The isProcessing() method (line 25) is consulted before each job execution. If a conversation is busy, the guard triggers retry logic with waitForIdle() and provides cleanup() utilities for housekeeping.

Frontend Integration

The renderer process interacts with the cron subsystem through React hooks and IPC bridges.

useCronJobs Hook

Located in src/renderer/pages/cron/hooks/useCronJobs.ts, this hook (lines 92-163) fetches jobs via ipcBridge.cron.listJobsByConversation and subscribes to real-time updates through onJobCreated, onJobUpdated, and onJobRemoved events. It exposes pauseJob, resumeJob, deleteJob, and updateJob mutations that sync with the backend.

UI Components

Code Examples

Creating a Scheduled Task from the Renderer

import { ipcBridge } from '@/common';
import { v4 as uuid } from 'uuid';

async function createDailyReminder(conversationId: string) {
  const job = await ipcBridge.cron.addJob.invoke({
    name: 'Morning Briefing',
    schedule: {
      kind: 'cron',
      expr: '0 9 * * *',
      tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
      description: 'Every day at 09:00',
    },
    message: 'Good morning! Here is your daily summary.',
    conversationId,
    agentType: 'openai',
    createdBy: 'user',
  });

  console.log('Cron job created:', job.id);
}

This IPC call routes to CronService.addJob in src/process/services/cron/CronService.ts (lines 70-126), which persists the job and initializes the native timer.

Using the useCronJobs Hook

import { useCronJobs } from '@/renderer/pages/cron/hooks/useCronJobs';
import { Button } from '@arco-design/web-react';

function CronControl({ conversationId }: { conversationId: string }) {
  const { jobs, hasJobs, pauseJob, resumeJob, deleteJob } = useCronJobs(conversationId);

  if (!hasJobs) return null;
  const job = jobs[0];

  return (
    <div>
      <span>{job.name} (next: {new Date(job.state.nextRunAtMs!).toLocaleString()})</span>
      {job.enabled ? (
        <Button onClick={() => pauseJob(job.id)}>Pause</Button>
      ) : (
        <Button onClick={() => resumeJob(job.id)}>Resume</Button>
      )}
      <Button status="danger" onClick={() => deleteJob(job.id)}>Delete</Button>
    </div>
  );
}

The hook subscribes to ipcBridge.cron.onJobUpdated events (see useCronJobs.ts lines 92-163), ensuring the UI reflects backend state changes immediately.

Handling Missed Executions

When the system resumes from sleep, CronService.handleSystemResume (lines 55-92) automatically detects missed jobs and inserts warning messages into the conversation. No manual intervention is required:

// This happens automatically in the main process
// CronService.handleSystemResume detects missed executions
// and calls insertMissedJobMessage for each missed job

The warning appears as a standard message in the chat history, alerting users that a scheduled task fired while the system was offline.

Summary

  • AionUi's cron subsystem consists of four layers: SQLite persistence (CronStore.ts), timer management (CronService.ts), concurrency protection (CronBusyGuard.ts), and React frontend hooks (useCronJobs.ts).
  • Job scheduling supports three timer types: standard cron expressions (kind: 'cron'), intervals (kind: 'every'), and one-time executions (kind: 'at').
  • Power management uses Electron's powerSaveBlocker to prevent app suspension while active jobs exist, and handleSystemResume catches up on missed executions after system sleep.
  • Busy-guard logic prevents race conditions by checking cronBusyGuard.isProcessing() before firing jobs, with automatic 30-second retries when conversations are processing other messages.
  • Real-time synchronization occurs through IPC events (onJobUpdated, onJobCreated, onJobRemoved) that keep the React UI in sync with the backend state without polling.

Frequently Asked Questions

How does AionUi persist scheduled tasks between app restarts?

AionUi stores all cron job definitions in a local SQLite database through the CronStore class in src/process/services/cron/CronStore.ts. When the application initializes, CronService.init() (lines 45-63) loads all enabled jobs from cronStore.listEnabled() and recreates their native timers. This ensures that scheduled tasks survive app restarts and system reboots without requiring external services.

What happens if a scheduled task fires while the conversation is busy processing another message?

The CronBusyGuard in src/process/services/cron/CronBusyGuard.ts tracks per-conversation processing flags. Before executing a job, CronService.executeJob() (lines 90-140) checks cronBusyGuard.isProcessing(). If the conversation is busy, the service increments the job's retryCount and schedules a retry in 30 seconds. This prevents message collisions while ensuring the task eventually executes once the conversation becomes idle.

Can AionUi handle scheduled tasks that were missed while the computer was asleep?

Yes. The CronService implements system resume detection in handleSystemResume() (lines 55-92). When the system wakes from sleep, this method identifies jobs that should have fired during the offline period. For each missed execution, it inserts a warning message into the conversation via insertMissedJobMessage() and restarts the job timers. Additionally, updatePowerBlocker() (lines 32-51) uses Electron's powerSaveBlocker.start('prevent-app-suspension') to minimize missed executions by keeping the app alive while jobs are active.

How do I create a recurring daily task from the React frontend?

Use the ipcBridge.cron.addJob.invoke method with kind: 'cron' in the schedule object. The useCronJobs hook in src/renderer/pages/cron/hooks/useCronJobs.ts provides a React-friendly abstraction, but you can also call the IPC bridge directly as shown in the code example above. Pass a standard cron expression (e.g., '0 9 * * *' for 9:00 AM daily), the target conversationId, and the message content. The CronService.addJob() method in the main process will persist the job to SQLite and initialize the native timer immediately.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →