Skip to content

Notebook jobs

A job runs a notebook to completion without a browser — on a cron schedule, or on demand from the UI, API, or CLI — and keeps a durable run history with the rendered outputs. marimo's deterministic DAG execution makes a notebook a good batch unit: a run executes marimo export html against saved notebook source and stores the result beside the run record. Editing, apps, and version history are untouched: a run never writes anything back to the notebook.

Enabling jobs

Jobs are off by default. Set MARIMOHUB_JOBS=on to enable the job API and UI, the Node maintenance scheduler or Cloudflare scheduled() handler, and the job.* project alert kinds. While off, the jobs routes answer 404, GET /api/v1/capabilities reports jobs.available: false, the UI hides its entry points, and stored job definitions and run history are left untouched so the feature can be turned back on without loss.

How it works

  • A job belongs to a notebook. Open Jobs & schedules from the notebook's actions menu (or the calendar icon on the notebook page) to define jobs: a name, an optional cron schedule with an IANA time zone, parameters, a timeout, a retry policy, and what to do when the previous run is still active. A job without a schedule is manual-only.
  • Runs pin notebook source, not every local workspace file. Each run uses a fresh copy-only sandbox (never a bucket mount) and records the notebook version it started from. Git-synced notebooks copy the complete immutable workspace of that version. Local notebooks copy the current workspace, then overlay the pinned version's notebook.py and pyproject.toml; other local workspace files therefore reflect their values when execution starts. The rendered HTML and captured stdout/stderr live under the run only. They are not notebook versions and never advance the notebook's head.
  • Parameters reach the notebook as mo.cli_args(). Each key=value becomes --key value after -- on the export command, so mo.cli_args().get("region") reads it. Values are strings and are never shell-interpolated. A manual run can override the job's stored parameters for that run only. Parameters are visible to project members who can read the job and run history, so they must not contain secrets.
  • Node dispatches from the maintenance replica. The replica running MARIMOHUB_RUN_MAINTENANCE=true evaluates schedules every MARIMOHUB_JOBS_TICK_SECONDS, dispatches queued runs under the concurrency caps, enforces run deadlines, and prunes old runs. Without that replica, a Node deployment accepts jobs but leaves manual triggers queued. The Cloudflare reference deployment instead dispatches from its platform scheduled() handler. See Operations.
  • Exactly once, never backfilled. Every scheduled fire claims an occurrence record keyed by its UTC minute, so two replicas evaluating the same schedule cannot both fire it. After an outage only the latest missed occurrence within MARIMOHUB_JOBS_CATCHUP_WINDOW_SECONDS runs; a three-day gap produces one catch-up run, not thousands.
  • Concurrency. MARIMOHUB_JOBS_MAX_CONCURRENT_RUNS bounds runs holding a sandbox across the deployment and MARIMOHUB_JOBS_MAX_CONCURRENT_RUNS_PER_PROJECT bounds each project's share; further runs wait in the queue, oldest first. Per job, the default policy skips a scheduled fire while the previous run is still active (the skip is recorded in the history); choose "run anyway" to let runs overlap.
  • Timeouts and retries. A run past its timeout (timeout_seconds, default MARIMOHUB_JOBS_DEFAULT_TIMEOUT_SECONDS, capped by MARIMOHUB_JOBS_MAX_TIMEOUT_SECONDS) has its sandbox destroyed and lands timed_out. A failed or timed-out run is retried up to max_retries times after backoff_seconds; each attempt is its own run linked to the previous one.
  • Retention. Run records and outputs older than MARIMOHUB_JOBS_RUN_RETENTION_DAYS are pruned by the Node maintenance cycle; the Cloudflare scheduled handler prunes with its fixed 30-day retention. Deleting a job removes its history immediately; soft-deleting a notebook or project cancels its active runs, and the hard-delete sweep reclaims the rest.
  • Compute image and profile. A run provisions exactly as a session start would: the notebook's base image choice (falling back to the deployment default when that image is no longer offered) and its compute profile when the deployment lets editors override (MARIMOHUB_COMPUTE_PROFILE_OVERRIDE=editors), else the default profile. The applied image, compute_profile, and compute_resources are recorded on the run for provenance, like on a session.
  • Audit events. job.create, job.update, and job.delete land in the project audit log with the actor; job.run.trigger and job.run.cancel record who started or stopped a run; job.run.finish records every outcome (status, attempt, exit code, sanitized error code, duration), attributed to the manual triggerer or to system for scheduled runs.

Run states

StatusMeaning
queuedWaiting for the scheduler to dispatch it (or for a retry's backoff to elapse).
provisioningA sandbox is being created and loaded.
runningmarimo export html is executing.
succeededEvery cell ran; the rendered output is available.
failedA cell raised, the export could not run, or the sandbox could not be prepared.
timed_outThe run exceeded its timeout and was reclaimed.
cancelledCancelled by an editor; its sandbox was destroyed.
skippedA scheduled fire declined because the previous run was still active (forbid policy).

When a cell raises, marimo still writes the rendered HTML, so a failed run usually has output showing where it stopped. The run's error carries a sanitized code (NOTEBOOK_FAILED, RUN_TIMED_OUT, …) — never provider messages or secrets. Editors can read the captured logs.

Who can do what

Actionviewereditormanageradmin
See jobs and run historyxxxx
Open a run's rendered outputxxxx
Create, edit, enable, or delete jobsxxx
Run a job now or cancel a runxxx
Read a run's stdout/stderr logsxxx

Triggering a job starts a kernel, so it takes the same editor gate as starting a session. Run history and outputs follow the static-snapshot posture (viewers can see rendered documents); logs are editor-only because stdout can echo environment values and tracebacks.

Security note. A run executes the notebook's code with the project's resolved integration secrets and federated credentials injected — exactly what an app session carries — regardless of who triggers it or when the schedule fires. The credentials are attributed to the manual triggerer, or to the job's author for scheduled runs. Treat a job definition as you would a shared app.

Notifications

With project alerts enabled, a job can send job.run.failed (once retries are exhausted) and job.run.succeeded events to the project's alert destinations that subscribe to them. The payload names the job, run, status, attempt, and sanitized error code.

API

All routes are under /api/v1/projects/{pid}/notebooks/{nid}/jobs and use the standard envelope. Job creation and manual run triggers accept an Idempotency-Key.

MethodPathNotes
GET/List jobs, oldest first, with cursor pagination.
POST/Create; validates cron/time zone/timeout.
GET/{jid}Read (ETag = updated_at).
PATCH/{jid}Partial update; null clears an optional field.
DELETE/{jid}Requires If-Match; cancels active runs, then deletes.
POST/{jid}/runsRun now; body { "parameters": {…} } overrides per run.
GET/{jid}/runsRun history, newest first, paginated.
GET/{jid}/runs/{rid}One run.
POST/{jid}/runs/{rid}/cancelCancel (runs are history; never deleted individually).
GET/{jid}/runs/{rid}/htmlRendered output, raw text/html.
GET/{jid}/runs/{rid}/logsCaptured logs, raw text/plain, editor-only.

The CLI exposes the same operations under jobs ….

Configuration

VariableDefaultMeaning
MARIMOHUB_JOBSoffTurns the feature on (on) or off (off).
MARIMOHUB_JOBS_TICK_SECONDS60Node only: scheduler interval on the maintenance replica.
MARIMOHUB_JOBS_MAX_CONCURRENT_RUNS5Node only: deployment-wide cap on runs holding a sandbox.
MARIMOHUB_JOBS_MAX_CONCURRENT_RUNS_PER_PROJECT2Node only: per-project share of the cap.
MARIMOHUB_JOBS_MAX_PER_NOTEBOOK5Node only: job definitions per notebook (0 = unlimited).
MARIMOHUB_JOBS_DEFAULT_TIMEOUT_SECONDS1800Node only: run deadline when the job sets none.
MARIMOHUB_JOBS_MAX_TIMEOUT_SECONDS14400Node only: ceiling on a job's timeout_seconds.
MARIMOHUB_JOBS_RUN_RETENTION_DAYS30Node only: how long run records and outputs are kept.
MARIMOHUB_JOBS_CATCHUP_WINDOW_SECONDS600Node only: how stale a missed occurrence may be and still fire, once.

On Node, the tuning variables apply only while MARIMOHUB_JOBS=on. Jobs grant editors nothing an edit session does not; unattended cost is bounded by the caps and timeouts, and a deployment can turn the feature off at any time.

Cloudflare Workers

The Workers reference deployment reads the same MARIMOHUB_JOBS toggle from its wrangler vars and ticks the scheduler from its five-minute cron trigger, so schedules finer than five minutes are Node-only, and a run is bounded by the Workers invocation limits. It ignores the tuning variables in the table and uses their listed defaults, except that tick cadence comes from the five-minute platform trigger.

See Configuration for the full reference.

Provider-agnostic. Deploy anywhere.