Background Agent Execution Triggers and Mechanisms in Rowboat: A Deep Dive into the Agent Schedule Runner

Rowboat's background agents are triggered by a file-driven polling loop in the Electron main process that evaluates schedules every 60 seconds, determines eligibility via shouldRunNow(), and executes eligible agents asynchronously while persisting runtime state to JSON files.

Rowboat, an open-source desktop AI assistant from rowboatlabs/rowboat, implements a lightweight yet robust scheduling system for background agents. Unlike heavy-weight job queues, Rowboat uses a simple file-based approach where a dedicated runner in the Electron main process polls configuration files, evaluates cron expressions and time windows, and triggers agent execution without blocking the main thread.

The Core Architecture: File-Driven Scheduling

Rowboat's scheduling architecture separates user configuration from runtime state, using two distinct JSON files in ~/.rowboat/config/.

Configuration via agent-schedule.json

The agent-schedule.json file serves as the declarative configuration where users define agent schedules, enablement status, and starting messages. Located at apps/x/packages/core/src/application/assistant/skills/background-agents/skill.ts, the schema supports three schedule types: cron for recurring expressions, window for random execution within a time range, and once for single executions at a specific time.

Runtime State in agent-schedule-state.json

The agent-schedule-state.json file, managed by apps/x/packages/core/src/agent-schedule/state-repo.ts, tracks the operational status of each agent. This read-only file stores status (idle, running, finished, failed), nextRunAt, lastRunAt, runCount, and error messages. The runner automatically updates this file after every execution attempt, providing the UI with real-time visibility into agent health.

Trigger Mechanisms: How Agents Are Activated

Rowboat employs multiple trigger mechanisms to evaluate and launch background agents, balancing periodic polling with immediate execution capabilities.

The Polling Loop (POLL_INTERVAL_MS)

The heart of the trigger system resides in apps/x/packages/core/src/agent-schedule/runner.ts. When the Electron app boots, initAgentRunner() in apps/x/apps/main/src/main.ts initializes an infinite while (true) loop that sleeps for POLL_INTERVAL_MS (60,000 milliseconds) between iterations. This polling architecture ensures the system checks every minute for agents that need execution without consuming excessive CPU resources.

Schedule Evaluation with shouldRunNow()

During each poll cycle, the runner invokes shouldRunNow() to determine agent eligibility. This function implements several guard clauses: disabled agents are skipped, agents with running status are skipped to prevent duplicates, once schedules execute only after their runAt timestamp passes and only if not previously triggered, and cron or window schedules fire when the current time reaches or exceeds nextRunAt. The calculateNextRunAt() function uses the cron-parser library to compute subsequent execution times for recurring schedules.

Immediate Execution via triggerRun()

For scenarios requiring immediate schedule updates, the triggerRun() function breaks the polling loop's sleep cycle. When users modify schedules through the UI, the IPC endpoint agent-schedule:updateAgent in apps/x/apps/main/src/ipc.ts updates the configuration file and invokes triggerRun(), forcing the runner to re-evaluate schedules instantly rather than waiting for the next minute tick.

Execution Mechanisms: From Trigger to Completion

Once an agent passes eligibility checks, Rowboat executes it using asynchronous patterns that prevent blocking the main process while maintaining robust error handling.

Asynchronous Agent Launching

The runAgent() function in runner.ts handles actual execution. It creates a run record, sends the optional startingMessage (defaulting to "go"), and invokes the core agent runtime via agentRuntime.trigger. Crucially, the polling loop does not await the agent's completion; instead, runAgent() returns immediately after triggering the runtime, allowing the runner to proceed with evaluating other agents while the background agent executes in parallel.

Timeout Handling and Failure Recovery

Rowboat implements comprehensive timeout handling through checkForTimeouts(), which runs periodically to detect agents exceeding TIMEOUT_MS (30 minutes). When a timeout occurs, the runner marks the agent status as failed, records the error, and schedules the next execution according to the recurrence pattern. This ensures that hung agents cannot block the schedule indefinitely and that failure states are visible in the state file for UI consumption.

State Updates and Persistence

After each execution attempt—whether successful, failed, or timed out—the runner updates agent-schedule-state.json through the state repository. For once schedules, successful completion sets the status to finished and prevents further execution. For recurring schedules, the runner calculates the next run time using calculateNextRunAt() and persists it to the state file, ensuring durable scheduling across application restarts.

IPC Integration: Controlling Schedules from the UI

The renderer process interacts with the scheduling system through IPC channels defined in apps/x/apps/main/src/ipc.ts.

// Example: Updating a schedule from the renderer
import { ipcRenderer } from 'electron';

async function updateAgentSchedule(agentName: string, schedule: any) {
  const result = await ipcRenderer.invoke('agent-schedule:updateAgent', {
    agentName,
    entry: {
      schedule,
      enabled: true,
      startingMessage: 'Process pending tasks'
    }
  });
  return result.success;
}

When the agent-schedule:updateAgent channel receives a request, it updates the configuration repository and immediately calls triggerRun(), ensuring that schedule changes take effect without waiting for the next polling cycle.

Summary

  • File-driven architecture: Rowboat uses agent-schedule.json for configuration and agent-schedule-state.json for runtime state, enabling durable scheduling without external databases.
  • Polling-based triggers: A 60-second polling loop in runner.ts evaluates schedules using shouldRunNow(), checking cron expressions, time windows, and execution eligibility.
  • Asynchronous execution: The runAgent() function triggers agents in fire-and-forget mode, allowing concurrent execution while the main loop continues polling.
  • Robust failure handling: A 30-minute timeout mechanism and automatic state updates ensure that failed or hung agents do not block the schedule.
  • Immediate updates: The triggerRun() function and IPC endpoints allow the UI to force immediate schedule re-evaluation when configurations change.

Frequently Asked Questions

How does Rowboat prevent duplicate agent executions?

Rowboat prevents duplicates through the shouldRunNow() function in runner.ts, which checks the status field in agent-schedule-state.json. If an agent's status is running, the runner skips the execution attempt. Additionally, for once schedules, the runner verifies that the agent has not previously triggered by checking the run history before allowing execution.

What happens if an agent exceeds the 30-minute timeout?

When an agent exceeds TIMEOUT_MS (30 minutes), the checkForTimeouts() function in runner.ts marks the agent's status as failed in the state file, records the timeout error message, and calculates the next scheduled run time using calculateNextRunAt(). This ensures the agent does not remain in a hung state indefinitely and follows its recurrence pattern for the next attempt.

Can I trigger a background agent immediately without waiting for the next poll?

Yes, you can trigger immediate execution by calling the triggerRun() function, which interrupts the polling loop's sleep cycle and forces an immediate evaluation of all schedules. When updating schedules through the UI, the agent-schedule:updateAgent IPC endpoint in ipc.ts automatically invokes triggerRun(), ensuring configuration changes take effect instantly rather than waiting for the next 60-second tick.

What schedule types are supported besides cron expressions?

Rowboat supports three schedule types defined in the skill documentation at apps/x/packages/core/src/application/assistant/skills/background-agents/skill.ts. In addition to cron expressions parsed by the cron-parser library, the system supports window schedules that execute at a random time within a specified daily window, and once schedules that execute a single time at a specific ISO timestamp and then mark the agent as finished.

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 →