Skip to content

Scheduled and Recurring Jobs

Jon edited this page Aug 30, 2026 · 1 revision

Purpose

Ash can act on a schedule, not just interactively. The AI can ask ash to run a prompt once in the future, or on a recurring cron schedule, and later list, inspect, change, or cancel those recurring jobs. Both mechanisms re-invoke the same ash binary as a normal OS-scheduled process — there is no separate daemon.

The three scheduling tools

These are published to the AI provider alongside the rest of ash's tools:

Tool Purpose Underlying mechanism
schedule_future_prompt Run a prompt once at a future time (for example, "in 10 minutes") A user launchd LaunchAgent (macOS)
schedule_recurring_prompt Run a prompt repeatedly on a cron schedule A user crontab entry
manage_recurring_jobs List, explain, modify, or cancel ash-owned recurring jobs Reads/writes the user crontab

The tool definitions, argument schemas, and dispatch live in tools.go; the handlers are callScheduleFuturePrompt, callScheduleRecurringPrompt, and callManageRecurringJobs in the same file. The supporting logic for building schedules, launch agents, and crontab lines lives in support.go.

Child agents (created via run_sub_agent) are blocked from using any of these tools — only the top-level ash process can schedule or manage jobs.

One-off scheduling: schedule_future_prompt

  1. The AI supplies a prompt, a when string (for example "now + 5 minutes", "in 10 minutes", or an RFC3339 timestamp), and an optional cwd.
  2. parseFutureScheduleTime in support.go resolves when into an absolute future time.
  3. buildFuturePromptLaunchAgent in support.go renders a launchd plist (buildLaunchAgentPlist) that runs the ash binary with the prompt as its argument, at the resolved date/time, in the requested working directory, with a captured environment (see Environment carried into scheduled runs below).
  4. The plist is written under ~/.ash/launchagents/ with a filename like com.user.gonetwork.<id>.plist, then loaded with launchctl bootstrap gui/<uid> <plist>.

This mechanism is macOS-specific because it relies on launchd/launchctl.

Recurring scheduling: schedule_recurring_prompt

  1. The AI supplies a prompt, a cron expression (5-field cron syntax or a macro like @daily), and optional cwd/purpose.
  2. validateCronExpr in support.go checks the expression is structurally valid.
  3. buildRecurringJobLine builds a recurringJobMetadata record (ID, cron, prompt, working directory, captured environment, purpose, creation time) and a corresponding crontab line via buildRecurringCrontabLine. The metadata is JSON-encoded, base64-encoded, and embedded as a trailing comment on the crontab line behind a marker prefix (# ash:job <id> <encoded-metadata>), so ash can find, decode, and manage its own jobs later without touching entries it didn't create.
  4. The current crontab is read with crontab -l (loadCurrentCrontab), the new line is appended (appendCrontabLine), and the result is written back with crontab - (writeCrontab).

Recurring scheduling relies only on cron/crontab, so it works on both macOS and Linux.

Managing recurring jobs: manage_recurring_jobs

callManageRecurringJobs in tools.go reads the crontab, decodes all ash-tagged entries with parseRecurringJobs, and supports four actions:

  • list: returns all ash-managed jobs as JSON.
  • explain: with an id, returns that job's cron schedule, working directory, purpose, and prompt; without an id, summarizes every job.
  • cancel: removes the crontab line matching the job id (removeRecurringJobFromCrontab) and rewrites the crontab.
  • modify: looks up the job by id, applies any supplied replacement fields (cron, prompt, cwd, purpose), rebuilds the invocation script and crontab line, replaces the old line (replaceRecurringJobLine), and rewrites the crontab.

Because job identification and matching is based on the # ash:job <id> marker, these actions never touch crontab entries that weren't created by ash.

Environment carried into scheduled runs

Scheduled invocations don't inherit your full interactive shell environment — only a small, explicit allowlist, built by schedulerEnvAllowlist in support.go: the AI provider endpoint/model/auth settings, SESSION_ID, HOME, PATH, and a handful of ash-specific tuning variables (timeouts, history size, logging, tool allowlist, max agents).

schedulerInvocationEnv builds on that allowlist for the actual scheduled run:

  • Generates a fresh SESSION_ID if one isn't already set.
  • Sets ASH_SCHEDULED_TASK=1 so the invoked process knows it's running as a scheduled task (checked by isScheduledTaskRun in support.go).
  • Forces verbose, JSON-formatted logging to a per-session log file under ~/.ash/logs/task_<session-id>.log, which is rotated once it exceeds 1 MB (rotatingSchedulerLogWriter in support.go).

buildScheduledInvocationScriptWithEnv in support.go turns the prompt, working directory, and this environment into a single shell command: cd <dir> && KEY=value ... '/path/to/ash' 'the prompt', with every value shell-quoted (shellQuote) to avoid injection through prompt text or environment values.

Notably, the ephemeral Connection-Sharing broker's socket path and capability token are not in the allowlist, so scheduled runs always use a direct HTTPS connection to the AI provider rather than reusing a live interactive shell's broker.

History and logs for scheduled runs

Scheduled runs keep their own history and log files, separate from interactive sessions:

  • History: ~/.ash/history/task_<session-id>.json
  • Logs (one-off only): ~/.ash/logs/task_<session-id>.log

Related reading

  • Connection-Sharing — how ash reuses HTTPS connections in interactive shells (and why scheduled runs don't).
  • tools.go and support.go — full source for everything described above.

Clone this wiki locally