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
- Open the Marimo Scheduler panel from the command palette
(
Ctrl+Shift+P→ "Open Marimo Scheduler Dashboard") - Switch to the YAML Schedules tab to paste/edit YAML
- 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:
- Resolves the notebook path relative to
root_dir - Injects parameters as
MARIMO_PARAM_<NAME>=<value>environment variables - Runs
marimo export html <notebook.py> -o <output.html>(or Python in script mode) - Writes a
.logsidecar with stdout/stderr - Raises
RuntimeErroron 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file marimo_jupyter_scheduler-0.1.3.tar.gz.
File metadata
- Download URL: marimo_jupyter_scheduler-0.1.3.tar.gz
- Upload date:
- Size: 203.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25caf322a948b9ea6bf79ed74cacc7c175d2ccfc57f661073ed2f0e94f7861da
|
|
| MD5 |
1a6f58ae0b1f31557281fe45b03ed176
|
|
| BLAKE2b-256 |
c0adb496ae043cfc3641868122b180ae0dba56b3f83c9c5aa124be2043b56fca
|
File details
Details for the file marimo_jupyter_scheduler-0.1.3-py3-none-any.whl.
File metadata
- Download URL: marimo_jupyter_scheduler-0.1.3-py3-none-any.whl
- Upload date:
- Size: 62.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87625abc30e5a8453a441fb76e70c4a921c9fcc400270fa4ca6f7ff526b40141
|
|
| MD5 |
37e54cf3f507b8eb7a353f40c31e5dff
|
|
| BLAKE2b-256 |
855961470208a2abc270fdeb8506dca543dca63aeb3e2a6bf661d5a8469ae7a1
|