Skip to main content

A JupyterLab extension to schedule Marimo notebooks in JupyterHub

Project description

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

Add to ~/.jupyter/jupyter_server_config.py:

c.SchedulerApp.scheduler_class = (
    "marimo_jupyter_scheduler.scheduler.MarimoScheduler"
)
c.Scheduler.execution_manager_class = (
    "marimo_jupyter_scheduler.executor.RoutingExecutionManager"
)
c.Scheduler.task_runner_class = (
    "marimo_jupyter_scheduler.task_runner.FixedTaskRunner"
)
c.SchedulerApp.environment_manager_class = (
    "marimo_jupyter_scheduler.environment.MarimoEnvironmentManager"
)
# Optional: use PostgreSQL
# c.Scheduler.db_url = "postgresql+psycopg2://user:pass@host:5432/scheduler"

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

In your JupyterHub singleuser image, add to /etc/jupyter/jupyter_server_config.py:

import os
c.SchedulerApp.scheduler_class = (
    "marimo_jupyter_scheduler.scheduler.MarimoScheduler"
)
c.Scheduler.execution_manager_class = (
    "marimo_jupyter_scheduler.executor.RoutingExecutionManager"
)
c.Scheduler.task_runner_class = (
    "marimo_jupyter_scheduler.task_runner.FixedTaskRunner"
)
c.SchedulerApp.environment_manager_class = (
    "marimo_jupyter_scheduler.environment.MarimoEnvironmentManager"
)
c.Scheduler.db_url = os.environ.get(
    "SCHEDULER_DB_URL",
    "sqlite:////home/jovyan/.jupyter-scheduler.db"
)

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

1. Bump the version in package.json:

"version": "0.1.1"

2. Build the production frontend:

source .venv/bin/activate
jlpm run build:prod

3. Build the distribution packages:

pip install build
python -m build

This produces dist/marimo_jupyter_scheduler-<version>.tar.gz and .whl.

4. Verify the wheel contains the labextension:

pip install check-wheel-contents
check-wheel-contents dist/*.whl
unzip -l dist/*.whl | grep labextension

You should see share/jupyter/labextensions/marimo-jupyter-scheduler/... entries.

5. (Optional) Test on TestPyPI first:

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

6. Upload to PyPI:

twine upload dist/*

License

BSD 3-Clause. See LICENSE.

Project details


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.1.1.tar.gz (202.7 kB 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.1.1-py3-none-any.whl (61.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: marimo_jupyter_scheduler-0.1.1.tar.gz
  • Upload date:
  • Size: 202.7 kB
  • 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.1.1.tar.gz
Algorithm Hash digest
SHA256 861187a470ba9e7493e976a1ec6fc62275135c20e95d35fc5320b9d162aa7b03
MD5 ab1107fb324040b8e88e991e9fa3759f
BLAKE2b-256 7b25f5800bd3feeb0f7bdfbf902582235511a20c9fb7335a367f7365358ca071

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for marimo_jupyter_scheduler-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 297071c6d18f045ac4519981f0bacc3fcf832803ce6a6ef23a36fd5ff6521a59
MD5 de5d110b93e809c230a99281ed9058a8
BLAKE2b-256 05eced611715797a1107aff892a0b712588750991fcc68eff15daf238851119e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page