Scheduling Scripts¶
Lamia supports scheduling .lm scripts to run automatically on your local machine using the native OS scheduler (launchd on macOS, systemd timers on Linux, Windows Task Scheduler on Windows).
All scheduled times are local machine time.
Adding a Schedule¶
The simplest way — use a preset:
Or run it every time you open your computer:
For precise control, use a cron expression:
The command resolves the script path and registers it with the OS scheduler. The script's parent directory becomes the working directory for execution.
Presets (--every)¶
| Preset | Behavior |
|---|---|
hour |
Every hour at :00 |
day |
Every day at 9:00 AM |
weekday |
Monday through Friday at 9:00 AM |
week |
Every Monday at 9:00 AM |
on-wake |
Once when the computer starts or wakes from sleep |
Options¶
| Flag | Default | Description |
|---|---|---|
--every |
— | Schedule preset (see above). Use this OR --cron. |
--cron |
— | Standard 5-field cron expression. Use this OR --every. |
--no-catch-up |
off | If set, missed runs (machine was off) are skipped |
By default, if the machine was off at the scheduled time, the missed run fires once when the machine wakes up. Pass --no-catch-up to disable this.
Legacy aliases are still accepted: hourly, daily, weekdays, weekly.
Listing Schedules¶
Shows all registered schedules with their cron, project path, and last run status:
[a3f7c2e1b9d0] daily_task.lm
schedule: day catch_up: True
path: /Users/you/project/pinterest_pin_publisher
last run: 2026-05-28T09:00:12+00:00 status: ok
If a script fails (e.g., file moved, runtime error), the status shows FAILED with the error detail.
Removing a Schedule¶
The <id> is shown in lamia schedule list output. This unloads the OS scheduler entry and removes the job from the registry.
Updating a Schedule¶
If you used a wrong cron expression or want to change cadence, update in one command:
Or switch to a custom cron:
You can also toggle catch-up behavior during update:
How It Works¶
Schedules are stored globally at ~/.lamia/schedules/ — one JSON file per job (<id>.json). Each file holds both the job configuration (script, cron, catch_up, project_root) and the last run status (timestamp, exit code, error). The OS scheduler invokes:
This means:
- One file per job — no companion status files, everything in
<id>.json - Logs are grouped per schedule id under
~/.lamia/logs/schedules/<id>/ - After each run, exit status is written back into the job file so
lamia schedule listcan display it - When you
lamia schedule remove <id>, both the OS entry and the file are deleted
Cron Expression Reference¶
┌───── minute (0-59)
│ ┌───── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌───── month (1-12)
│ │ │ │ ┌───── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
Common patterns:
| Expression | Meaning |
|---|---|
0 9 * * * |
Daily at 9:00 AM |
0 */6 * * * |
Every 6 hours |
30 8 * * 1-5 |
Weekdays at 8:30 AM |
0 0 1 * * |
First of each month at midnight |
OS-Specific Behavior¶
macOS (launchd)¶
Creates a plist at ~/Library/LaunchAgents/com.lamia.schedule.<id>.plist. Uses StartCalendarInterval for time-based scheduling. Catch-up of missed runs is handled by Lamia at runtime (comparing last run time to current time).
Linux (systemd)¶
Creates a .service and .timer unit in ~/.config/systemd/user/ named lamia-schedule-<id>. Uses Persistent=true to catch up on missed runs after boot.
Windows (Task Scheduler)¶
Creates a task in Lamia\<id> via schtasks.exe. The task runs with the user's permissions.
Best Practices for Scheduled Scripts¶
Lamia tracks only the last run of each scheduled job — its timestamp, exit code, and error message. Previous run information is overwritten. You can access the schedule ID via schedule.id in your script to store it in a persistent store.
If you want visibility into run history, keep your own per-run records in a persistent store (file, database, or API) so you can audit trends and troubleshoot faster.
Example — deterministic CSV append (no LLM call):
from datetime import datetime
class RunResult(BaseModel):
timestamp: str
job_id: str
items_processed: int
success: bool
error: str
def log_run(job_id, items, success, error="") -> File(CSV[RunResult], "runs/history.csv", append=True):
return f"{datetime.now().isoformat()},{job_id},{items},{success},{error}"
You can also chain file-read mode with deterministic append:
from datetime import datetime
class RunResult(BaseModel):
timestamp: str
job_id: str
items_processed: int
success: bool
error: str
job_id = schedule.id # schedule.id is the ID of the scheduled job provided by Lamia
items_processed = 100 # Should be set by your script
success = True # Should be set by your script
error = None # Should be set by your script
def append_history(payload: JSON) -> File(CSV[RunResult], "runs/history.csv", append=True):
return f"{datetime.now().isoformat()},{job_id},{items_processed},{success},{error:None}"
Recommended practices:
-
Log each run's outcome — append to CSV rows or write to a database table. Include timestamps, what was processed, and success/failure per item.
-
Store the run timestamp and exit status alongside your data. Lamia keeps only the last one, so if you need a history view, maintain your own.
-
Use idempotent and resumable operations — catch-up can re-trigger a missed run, and a run can fail halfway. Design steps so retries do not duplicate side effects, and partial failures can resume safely from checkpoints.
-
Keep scripts self-contained — the scheduled script runs from its parent directory with no interactive terminal. Avoid prompts, ensure credentials are pre-configured, and handle network failures with retries.
-
Test manually first — run
lamia --file your_script.lmby hand before scheduling. If it works interactively, it will work on schedule.
Cloud Scheduling¶
For always-on schedules that don't depend on your local machine being awake, use cloud scheduling with the --remote flag:
Run this one-time cloud invocation first to verify cloud permissions and runtime behavior. Then create the schedule:
Cloud jobs are guaranteed to run on time — no catch-up needed, no local machine required. All other commands (list, update, remove) automatically detect whether a job is local or cloud.
Migrating Between Local and Cloud¶
schedule add matches an existing schedule by script filename and project path — running it again for the same script updates that schedule instead of creating a second one. Add --remote (or drop it) to move an existing schedule to the other backend, using the same command you'd use to create it:
The cron expression, catch-up setting, and the script itself don't need to match what was there before — a migration can change any of them in the same command.
The old backend's job is torn down and the new one created under the same schedule ID. For local -> cloud that's a fresh deploy; for cloud -> local the Cloud Run Job and its execution history are deleted, since nothing carries over across backends.
Migrating removes the script from the old environment before scheduling it in the new one, so it never ends up running in both places at once. That matters for anything with side effects that shouldn't repeat: sending an email, posting a pin, writing to an external system.
This match is per machine — it only sees schedules recorded in your own ~/.lamia/schedules/. If a teammate created a cloud schedule for a script from their machine, running schedule add for that same script on yours won't detect it and will create a second, independent schedule rather than migrating theirs. Coordinate with your team before scheduling a script that's already running on a schedule elsewhere.
If the scheduled script reads credentials from the environment, only keys listed in cloud.secrets are sent to Secret Manager at deploy time — the container itself never receives your .env file. Keys not listed in cloud.secrets are not available in cloud runtime. Removing the schedule removes the secrets it was using. See lamia-cloud — Secrets.
Cloud scheduling requires the lamia-cloud package.
See the lamia-cloud user guide for installation, one-time cloud runs, and scheduler setup.
For team deployments via Git-based CI pipelines, see Git-Based Deployment.
Resource Lifecycle¶
If a script file is renamed, moved, or deleted, any schedule pointing to it
shows SOURCE_MISSING in lamia schedule list. To clean up these entries:
Unused cloud resources (containers, images) are automatically removed after 30 days of inactivity. Active schedules and triggers are never affected.
Troubleshooting¶
- Script not found: If you moved the project after scheduling, the job will fail. Use
lamia schedule listto see the stale path, thenremoveandaddwith the new location. - Check logs: Use
lamia schedule listto get the job id, then runlamia schedule logs <id>. Local jobs write to~/.lamia/logs/schedules/<id>/schedule.log. - Force fresh login: If a web automation script's session expired, delete
.lamia_sessions/and run the script manually once to re-authenticate before the next scheduled run. - Schedule stopped firing (macOS): If
lamia schedule listshows a stalelast rundate and no new runs occur, a previous execution may have hung. Check withlaunchctl list <label>— if a PID is shown, the old process is still alive. Kill it withkill <pid>and the schedule will resume on its next interval. Lamia arms a 24-hour watchdog timer (SIGALRM) for scheduled runs to prevent this, and launchd'sExitTimeOutprovides an additional safety net. - Cloud timeout differs from local: Local scheduled runs use a 24-hour watchdog. Cloud Run Jobs default to a 1-hour task timeout to reduce accidental billing from hung jobs. For heavy workloads, raise
cloud.resources.task_timeout_secondsinconfig.yaml(up to 7 days). - Cloud: "requires lamia-cloud": Run
pip install "lamia-lang[cloud]". See lamia-cloud docs.