This release is a pre-release and may not be stable for production use.
datasette-cron
Database-backed scheduled tasks for Datasette.
Plugins can register handler functions, then create tasks that run on a schedule. Tasks persist across restarts, support cron expressions and intervals, and record execution history.
Installation
pip install datasette-cron
Quick Start
A plugin registers a handler function and creates a task that runs on a schedule:
from datasette import hookimpl
@hookimpl
def cron_register_handlers(datasette):
async def my_handler(datasette, config):
db = datasette.get_database(config["database"])
await db.execute_write("INSERT INTO log (message) VALUES ('tick')")
return {"my-handler": my_handler}
@hookimpl
def startup(datasette):
async def inner():
scheduler = datasette._cron_scheduler
await scheduler.add_task(
name="log-every-minute",
handler="myplugin:my-handler",
schedule={"interval": 60},
config={"database": "mydb"},
)
return inner
How It Works
- Startup: datasette-cron creates a
Scheduleratdatasette._cron_schedulerand collects handlers from all plugins via thecron_register_handlershook - First request: The scheduler loop starts (via
asgi_wrapper), ticking every ~1 second - Each tick: Queries
datasette_cron_tasksfor tasks wherenext_run_at <= nowandenabled = 1 - Execution: Looks up the handler function, calls it with
(datasette, config), records the result indatasette_cron_runs - Next run: Advances
next_run_atbased on the schedule
Plugin Hook
cron_register_handlers(datasette)
Return a dict mapping handler names to callable functions:
@hookimpl
def cron_register_handlers(datasette):
return {
"check-feeds": check_feeds_handler,
"cleanup": cleanup_handler,
}
Handlers are registered with a plugin-derived prefix. If your plugin module is
datasette_myplugin, handlers are accessible as myplugin:check-feeds and
myplugin:cleanup.
Handler Signature
async def my_handler(datasette, config):
"""
datasette: the Datasette instance
config: dict from the task's config field
"""
pass
Handlers can be sync or async.
Scheduler API
Access the scheduler via datasette._cron_scheduler after startup.
add_task()
Create or update a task (idempotent upsert). If the task already exists,
next_run_at is preserved.
await scheduler.add_task(
name="my-task",
handler="myplugin:my-handler",
schedule={"interval": 300}, # every 5 minutes
config={"key": "value"}, # passed to handler
timezone="America/New_York", # optional
overlap="skip", # "skip" prevents overlapping runs
retry={"max_retries": 3, "backoff": "exponential"},
)
Schedule Types
Interval (seconds):
schedule={"interval": 60} # every 60 seconds
schedule={"interval": 1} # every second
Cron expression:
schedule="0 8 * * *" # daily at 8am
schedule="*/5 * * * *" # every 5 minutes
RFC 5545 RRULE:
schedule={"rrule": "FREQ=WEEKLY;BYDAY=MO"}
Other Methods
await scheduler.remove_task("my-task")
await scheduler.trigger_task("my-task") # run immediately
await scheduler.enable_task("my-task")
await scheduler.disable_task("my-task")
await scheduler.update_task("my-task", schedule={"interval": 10})
Data Models
Query results from InternalDB return typed dataclasses:
from datasette_cron.models import CronTask, CronRun
task: CronTask = await scheduler.internal_db.get_task("my-task")
print(task.name, task.handler, task.next_run_at, task.last_status)
runs: list[CronRun] = await scheduler.internal_db.get_runs("my-task")
for run in runs:
print(run.started_at, run.status, run.duration_ms)
CronTask
| Field | Type | Description |
|---|---|---|
name |
str |
Unique task identifier |
handler |
str |
Handler reference (e.g., "myplugin:my-handler") |
config |
dict |
JSON config passed to handler |
schedule_type |
str |
"interval", "cron", or "rrule" |
schedule_config |
str |
JSON schedule parameters |
timezone |
str | None |
IANA timezone |
overlap_policy |
str |
"skip" or "allow" |
retry_max |
int |
Max retry attempts |
retry_backoff |
str |
"exponential" or "linear" |
enabled |
bool |
Whether task is active |
next_run_at |
str | None |
ISO timestamp of next scheduled run |
last_run_at |
str | None |
ISO timestamp of last run |
last_status |
str | None |
"success" or "error" |
CronRun
| Field | Type | Description |
|---|---|---|
id |
int |
Auto-increment ID |
task_name |
str |
Which task this run belongs to |
started_at |
str |
ISO timestamp |
finished_at |
str | None |
ISO timestamp |
status |
str |
"running", "success", or "error" |
error_message |
str | None |
Error details on failure |
attempt |
int |
Retry attempt number |
duration_ms |
int | None |
Execution time in milliseconds |
REST API
| Method | Endpoint | Description |
|---|---|---|
| GET | /-/api/cron/tasks |
List all tasks |
| GET | /-/api/cron/tasks/{name} |
Task detail |
| GET | /-/api/cron/tasks/{name}/runs |
Run history |
| POST | /-/api/cron/tasks/{name}/trigger |
Trigger immediate run |
| POST | /-/api/cron/tasks/{name}/enable |
Enable/disable task |
All endpoints require the datasette-cron-access permission.
Database Tables
Stored in Datasette's internal database:
datasette_cron_tasks — task definitions and scheduling state
datasette_cron_runs — execution history with timing, status, and errors
Development
just dev # start dev server
just test # run tests
just format # format code (backend + frontend)
just check # lint + type check (backend + frontend)
Metadata
Release files for datasette-cron 0.0.1a2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| datasette_cron-0.0.1a2.tar.gz | 52.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| datasette_cron-0.0.1a2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 114.9 kB
Release files / datasette_cron-0.0.1a2.tar.gz
| Download URL | datasette_cron-0.0.1a2.tar.gz |
|---|---|
| Size | 52.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b9d3c4ee4f2a287ce374590a1e1084c502efb89642c77e4ae85cda37e64d52e4
|
|
BLAKE2b-256 checksum How to use checksums |
bd784f13e053664a5d33b5becb00ec75ac729d583686d4db9dddbacd971c8507
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 13, 2026.
Transparency logRelease files / datasette_cron-0.0.1a2-py3-none-any.whl
| Download URL | datasette_cron-0.0.1a2-py3-none-any.whl |
|---|---|
| Size | 62.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fb41ae1aefcf424304097ef9445e0c6039595ca7c4964379ecc359def2e627d7
|
|
BLAKE2b-256 checksum How to use checksums |
1d329eac4e2eaa38a3410e7a8b903ae8f8c63cae186ef3052aa3c859e243e510
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 13, 2026.
Transparency log