One-shot and scheduled service types in app.toml
Today all services in app.toml are long-running processes — they start and stay up. The concurrency modes (auto, fixed) control how many instances run, but there's no concept of a process that runs once and exits, or runs on a schedule.
The need
Apps commonly need:
- One-shot commands: database migrations, cache warming, data imports — run once per deploy (or on demand), exit when done
- Scheduled jobs: cleanup tasks, report generation, health checks — run on a recurring schedule
Today people work around this with background goroutines in the app process, sleep loops in "worker" services, or external cron hitting HTTP endpoints. All of these are fragile and lack observability.
What this could look like
One-shot commands as a service type:
[services.migrate]
command = "python manage.py migrate"
type = "oneshot" # runs once per deploy, must exit 0 to proceed
Scheduled jobs building on one-shot, with two scheduling modes:
# Simple interval — just a Go duration
[services.cleanup]
command = "./bin/cleanup-old-sessions"
type = "scheduled"
every = "6h"
# Calendar-based — systemd OnCalendar syntax
[services.reports]
command = "./bin/weekly-report"
type = "scheduled"
schedule = "Mon *-*-* 09:00:00"
Scheduling syntax design note
We want to be a human-friendly platform, so cron syntax (0 9 * * 1) is not ideal. Two approaches:
everyfor intervals: Go-style durations (6h,30m,24h). Dead simple, no ambiguity.schedulefor calendar times: systemd OnCalendar syntax preferred over cron. It reads left-to-right (Mon *-*-* 09:00:00= "Monday at 9am") vs. cron's backwards field order. Also has nice keywords (daily,weekly,monthly,hourly). Well-documented viaman systemd.time. Cron support as a possible fallback if there's demand, but OnCalendar is the better default.
every and schedule would be mutually exclusive.
Platform responsibilities
- One-shot: Spin up a sandbox, run the command, capture exit code. For deploy-time tasks, gate the deploy on success. For on-demand tasks, expose via
miren runor similar. - Scheduled: All of the above plus: scheduling, single-execution guarantee (no double-runs), execution history, retry policy. Integrates with the sandbox pool model — spin up a sandbox for the job, tear it down after.
Building blocks
One-shot is the foundation — scheduled is just "one-shot + a timer + distributed dedup." Get one-shot right first.
For the scheduling layer, the CronScheduler in internal/bgtask/cron.go (cloud PR #112) solves a similar problem at the infrastructure level with Valkey-based distributed dedup. Different layer (cloud infra vs. app platform), but the design thinking around single-execution guarantees applies.
Relation to existing tickets
Supersedes MIR-81 ("FaaS: Cron-like execution") which was scoped to FaaS. This is broader — it's a first-class service type, not a FaaS feature.