Submit an issue View all issues Source
MIR-853

One-shot and scheduled service types in app.toml

Open public
phinze phinze Opened Mar 20, 2026 Updated Jul 13, 2026

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:

  • every for intervals: Go-style durations (6h, 30m, 24h). Dead simple, no ambiguity.
  • schedule for 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 via man 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 run or 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.