Repository navigation
Scheduled and Recurring Jobs
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.
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.
- The AI supplies a
prompt, awhenstring (for example"now + 5 minutes","in 10 minutes", or an RFC3339 timestamp), and an optionalcwd. -
parseFutureScheduleTimein support.go resolveswheninto an absolute future time. -
buildFuturePromptLaunchAgentin support.go renders a launchd plist (buildLaunchAgentPlist) that runs theashbinary 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). - The plist is written under
~/.ash/launchagents/with a filename likecom.user.gonetwork.<id>.plist, then loaded withlaunchctl bootstrap gui/<uid> <plist>.
This mechanism is macOS-specific because it relies on launchd/launchctl.
- The AI supplies a
prompt, acronexpression (5-field cron syntax or a macro like@daily), and optionalcwd/purpose. -
validateCronExprin support.go checks the expression is structurally valid. -
buildRecurringJobLinebuilds arecurringJobMetadatarecord (ID, cron, prompt, working directory, captured environment, purpose, creation time) and a corresponding crontab line viabuildRecurringCrontabLine. 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. - The current crontab is read with
crontab -l(loadCurrentCrontab), the new line is appended (appendCrontabLine), and the result is written back withcrontab -(writeCrontab).
Recurring scheduling relies only on cron/crontab, so it works on both macOS and Linux.
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 anid, returns that job's cron schedule, working directory, purpose, and prompt; without anid, summarizes every job. -
cancel: removes the crontab line matching the jobid(removeRecurringJobFromCrontab) and rewrites the crontab. -
modify: looks up the job byid, 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.
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_IDif one isn't already set. - Sets
ASH_SCHEDULED_TASK=1so the invoked process knows it's running as a scheduled task (checked byisScheduledTaskRunin 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 (rotatingSchedulerLogWriterin 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.
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
- 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.