Skip to main content

marimo-jupyter-scheduler

A JupyterLab extension that schedules Marimo notebooks as recurring jobs inside JupyterLab and JupyterHub environments.

Built on top of the official jupyter-scheduler package, so it inherits a battle-tested REST API, APScheduler cron engine, and SQLAlchemy-backed job store (SQLite by default, PostgreSQL optional).


Features

Feature Description
Marimo-native executor Runs .py Marimo notebooks via marimo export html or as plain Python scripts
YAML schedules Define jobs in *.marimo-schedule.yml files (GitHub Actions syntax) — auto-detected on startup
GUI dashboard JupyterLab panel showing running / failed / completed jobs with live refresh
SQLite / PostgreSQL Switch backends via a single env var (SCHEDULER_DB_URL)
Parameter injection Pass parameters to notebooks as MARIMO_PARAM_* env vars + ${TODAY} / ${VAR} substitution
Last-run injection Optionally pass the datetime of the last successful run as a parameter (_last_run)

Installation (from source)

Prerequisites: Node ≥ 18, Python ≥ 3.9, JupyterLab 4.

pip install jupyterlab hatch
npm install -g jlpm

# Clone
git clone https://github.com/your-org/marimo-jupyter-scheduler
cd marimo-jupyter-scheduler

# Install Python deps + build the frontend
pip install -e ".[dev]"
jlpm install
jlpm run build

# Enable the extension
jupyter labextension develop --overwrite .
jupyter server extension enable marimo_jupyter_scheduler

Configure the Marimo executor

Nothing to do. pip install writes the four class overrides to {sys.prefix}/etc/jupyter/jupyter_server_config.py and the server loads them on startup. To confirm they took effect:

python -c "import marimo_jupyter_scheduler.executor as e; \
  print(e.RoutingExecutionManager.validate(e.RoutingExecutionManager, '/nonexistent'))"
# True

The shipped defaults sit below ~/.jupyter and /etc/jupyter in precedence, so you can still override any of them, e.g. to use PostgreSQL:

c.Scheduler.db_url = "postgresql+psycopg2://user:pass@host:5432/scheduler"

A source checkout installed with pip install -e . may not place the file. If the check above fails, copy jupyter-config/jupyter_server_config.py into a config directory by hand.


Defining schedules

Option A — YAML file (config-as-code)

Create a *.marimo-schedule.yml file anywhere in your workspace:

version: "1"

schedules:
  - name: daily-sales-report
    notebook: notebooks/sales_report.py
    cron: "0 9 * * 1-5"       # weekdays at 09:00
    timezone: "Europe/Berlin"
    output_formats:
      - html
    parameters:
      date: "${TODAY}"         # substituted at runtime
      _last_run: "LAST_RUN_AT" # injects last successful run's datetime as MARIMO_PARAM_LAST_RUN_AT
    tags:
      - daily
    enabled: true

The extension auto-detects and imports this file on startup and on every save.

Option B — GUI

  1. Open the Marimo Scheduler panel from the command palette (Ctrl+Shift+P → "Open Marimo Scheduler Dashboard")
  2. Switch to the YAML Schedules tab to paste/edit YAML
  3. Or use the built-in jupyter-scheduler UI to create jobs directly

Architecture

marimo-jupyter-scheduler/
├── marimo_jupyter_scheduler/       # Python backend
│   ├── executor.py                 # MarimoExecutionManager (core)
│   ├── scheduler.py                # MarimoScheduler (fixes copy_input_file + update_job_definition)
│   ├── task_runner.py              # FixedTaskRunner (fixes SQLite threading bug)
│   ├── environment.py              # MarimoEnvironmentManager (registers output formats)
│   ├── yaml_jobs.py                # YAML parser + ${VAR} substitution
│   ├── yaml_watcher.py             # File watcher (watchdog)
│   └── handlers.py                 # Extra REST endpoints (/marimo-scheduler/api/v1/)
├── src/                            # TypeScript / React frontend
│   ├── index.ts                    # JupyterLab plugin entry point
│   ├── dashboard.tsx               # Dashboard React component
│   ├── api.ts                      # API client
│   └── components/
│       ├── JobsTable.tsx
│       ├── DefinitionEditor.tsx
│       └── StatusBadge.tsx
└── examples/
    ├── sample.marimo-schedule.yml
    └── sales_report.py             # Parameterised Marimo notebook demo

How the executor works

MarimoExecutionManager subclasses jupyter_scheduler.executors.ExecutionManager. When a scheduled job fires, it:

  1. Resolves the notebook path relative to root_dir
  2. Injects parameters as MARIMO_PARAM_<NAME>=<value> environment variables
  3. Runs marimo export html <notebook.py> -o <output.html> (or Python in script mode)
  4. Writes a .log sidecar with stdout/stderr
  5. Raises RuntimeError on non-zero exit → jupyter-scheduler marks the job FAILED

Special parameters

Parameter Behaviour
_last_run: "<NAME>" Injects last COMPLETED run's end_time as MARIMO_PARAM_<NAME> (ISO-8601 UTC)
_env: {KEY: value} Injected as plain env vars (no MARIMO_PARAM_ prefix)
_python: "/path" Overrides the Python interpreter / marimo binary for this job
_timeout: 3600 Subprocess timeout in seconds (default: 3600)

Environment variables

Variable Default Description
SCHEDULER_DB_URL sqlite:///~/.jupyter-scheduler.db SQLAlchemy database URL
SCHEDULER_MAX_CONCURRENT 5 Max parallel jobs
LOG_LEVEL INFO Python logging level

Running tests

pip install -e ".[dev]"
pytest tests/ -v

JupyterHub deployment

pip install marimo-jupyter-scheduler in the singleuser image is enough — the class overrides ship with the package. Only add config for what you want to change, in /etc/jupyter/jupyter_server_config.py:

import os
c.Scheduler.db_url = os.environ.get(
    "SCHEDULER_DB_URL",
    "sqlite:////home/jovyan/.jupyter-scheduler.db"
)

Put deployment config in /etc/jupyter, never ~/.jupyter. Hubs that mount a user's home directory over /home/jovyan at spawn time will shadow anything written to ~/.jupyter when the image was built, and the server starts with the stock jupyter-scheduler classes. That failure is silent: validate() rejects marimo .py notebooks, TaskRunner.process_queue swallows the exception, and schedules simply never fire.

Verify inside a running container:

python -c "import marimo_jupyter_scheduler.executor as e; \
  print(e.RoutingExecutionManager.validate(e.RoutingExecutionManager, '/nonexistent'))"
# True

For a shared PostgreSQL backend (so all Hub users share one job store), set SCHEDULER_DB_URL to a PostgreSQL connection string in the JupyterHub singleuser environment config.


Publishing to PyPI

Releases are published by the Release workflow (.github/workflows/release.yml), which runs on every push to the release branch. It builds the frontend, runs the tests, verifies the wheel contains the labextension, and uploads to PyPI using the PYPI_API_TOKEN repository secret. Do not run twine upload by hand.

1. Bump the version in package.json — the only manual step. pyproject.toml reads it via hatch-nodejs-version:

"version": "0.2.0"

Commit that to main. PyPI refuses to re-upload a version that already exists, so a release with an unchanged version will fail at the last step.

2. Push main to release:

git push origin main:release

Watch the run with gh run watch --branch release. If the build, tests, or wheel check fail, nothing is uploaded and you can fix and re-push the same version. Once the upload succeeds the version is permanent.

3. Verify:

pip install --upgrade marimo-jupyter-scheduler
python -c "import marimo_jupyter_scheduler.executor as e; \
  print(e.RoutingExecutionManager.validate(e.RoutingExecutionManager, '/nonexistent'))"
# True

Testing a build locally

To inspect a wheel without publishing:

jlpm run build:prod        # required — a wheel without this ships no frontend
python -m build
check-wheel-contents dist/*.whl
unzip -l dist/*.whl | grep labextension

You should see share/jupyter/labextensions/marimo-jupyter-scheduler/... entries. To exercise the full install path, upload to TestPyPI rather than PyPI:

twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ marimo-jupyter-scheduler

License

BSD 3-Clause. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

marimo_jupyter_scheduler-0.2.2.tar.gz (1.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

marimo_jupyter_scheduler-0.2.2-py3-none-any.whl (1.8 MB view details)

Uploaded Python 3

File details

Details for the file marimo_jupyter_scheduler-0.2.2.tar.gz.

File metadata

  • Download URL: marimo_jupyter_scheduler-0.2.2.tar.gz
  • Upload date:
  • Size: 1.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for marimo_jupyter_scheduler-0.2.2.tar.gz
Algorithm Hash digest
SHA256 e8f1d193813576f0d3207658aa8babb93aee2c35144038d547afcb71240e5ad9
MD5 4e6b2b9dbbb6ccb31b0196116de6b4a4
BLAKE2b-256 ca3d711b9bfe4b913c1ce75479c701a5ed2c4cb672b4fd81e0486c23cb95eb91

See more details on using hashes here.

File details

Details for the file marimo_jupyter_scheduler-0.2.2-py3-none-any.whl.

File metadata

File hashes

Hashes for marimo_jupyter_scheduler-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cb431d2b79f271d1f126a0f852e6561a82f5d40d50ac9ddc9e3bf740db1ad64c
MD5 15da9563419e55fc77df1583bb1b586d
BLAKE2b-256 9e68b9e4c26c51377be27901de70a0555ff100ca8e67b480b4bc485e08d6ea48

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.4

2 files

0.2.3

2 files

This release

0.2.2 This release

2 files

0.2.1

2 files

0.2.0

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page