TinySupervisor
Define workflows in Python, run anything you want. Tiny to fit in a container, but with observability built in.
A Python library inspired by Supervisor and workflow APIs like Airflow. It aims to be a simple way to define workflows to be easily included into a single container.
Features
- Task types: Job (one-shot), CronJob (recurring), RecurrentJob (dependency-driven), Service (long-running)
- DAG dependencies:
wait_for="start"/wait_for="completed" - Built-in observability:
/health,/state,/metricsHTTP endpoints - Per-task log capture: each run's output is written to a log file, optionally streamed to the console
- Zero config: single container friendly
Install
Requires Python 3.14+.
(soon, not published yet)
uv add tinysupervisor
# or
pip install tinysupervisor
Quick start
A minimal workflow with two jobs — the second waits for the first to complete:
# runnable: quickstart
import os
import sys
from tinysupervisor import Job, init_supervisor
def main() -> int:
supervisor = init_supervisor()
supervisor.set_http_port(int(os.environ.get("TINYSUPERVISOR_HTTP_PORT", "8081")))
supervisor.set_heartbeat_interval("100ms")
supervisor.register(Job(name="prepare", command="echo preparing"))
supervisor.register(
Job(
name="build",
command="echo building",
depends=["prepare"],
wait_for="completed",
)
)
return supervisor.start()
if __name__ == "__main__":
sys.exit(main())
The supervisor starts an HTTP server (default port 8081), runs the tasks in dependency order, and exits automatically once all tasks complete:
prepareruns firstbuildwaits forprepareto complete, then runs- The supervisor exits with code
0(or1if any task ends FATAL)
Tasks
Job
A one-shot task that runs once and completes (or fails):
from tinysupervisor import Job
Job(name="backup", command="tar -czf backup.tar.gz /data")
Jobs can also run a Python function via executable:
Job(name="greet", executable=lambda: print("hello"))
CronJob
A task that runs on a recurring interval, optionally bounded by run_until:
# runnable: cron
import os
import sys
from tinysupervisor import Job, CronJob, init_supervisor
def main() -> int:
supervisor = init_supervisor()
supervisor.set_http_port(int(os.environ.get("TINYSUPERVISOR_HTTP_PORT", "8081")))
supervisor.set_heartbeat_interval("100ms")
supervisor.register(Job(name="init", command="echo init"))
supervisor.register(
CronJob(
name="heartbeat",
interval="1s",
run_until=3,
command="echo beat",
depends=["init"],
wait_for="completed",
)
)
return supervisor.start()
if __name__ == "__main__":
sys.exit(main())
run_until can be an integer (max number of runs) or a duration string like "30s".
Service
A long-running task that the supervisor keeps alive until stopped:
Service(name="server", command="python3 -m http.server 8080")
Services stay running until the supervisor receives SIGINT or SIGTERM.
When a service exits unexpectedly the supervisor can optionally restart it
(autorestart=True).
Dependencies
Tasks can depend on others. The wait_for parameter controls when the dependent task starts:
| Value | Meaning |
|---|---|
start |
Start as soon as the dependency has started |
completed |
Wait until the dependency completes (default) |
# runnable: run_after
import os
import sys
from tinysupervisor import Job, CronJob, RecurrentJob, init_supervisor
def main() -> int:
supervisor = init_supervisor()
supervisor.set_http_port(int(os.environ.get("TINYSUPERVISOR_HTTP_PORT", "8081")))
supervisor.set_heartbeat_interval("100ms")
supervisor.register(Job(name="seed", command="echo seed"))
supervisor.register(
CronJob(
name="tick",
interval="1s",
run_until=2,
command="echo tick",
depends=["seed"],
wait_for="completed",
)
)
supervisor.register(
RecurrentJob(
name="after_tick",
command="echo after tick",
depends=["tick"],
)
)
return supervisor.start()
if __name__ == "__main__":
sys.exit(main())
RecurrentJob runs every time its dependencies produce a new event (by default
after each completed run), and completes once all of its dependencies complete.
Set trigger_mode="after_start" to run whenever a dependency starts instead.
You can also chain tasks automatically with auto_dependency_mode:
supervisor.auto_dependency_mode(mode="register_order", wait_for="start")
# Each task registered after this call depends on the previous one (start mode).
Environment variables
Pass environment variables to a task with the env parameter. The child
process inherits the parent environment with the given values overlaid:
# runnable: env
import os
import sys
from tinysupervisor import Job, init_supervisor
def main() -> int:
supervisor = init_supervisor()
supervisor.set_http_port(int(os.environ.get("TINYSUPERVISOR_HTTP_PORT", "8081")))
supervisor.set_heartbeat_interval("100ms")
supervisor.register(
Job(
name="greet",
command="echo Hello $GREETING",
env={"GREETING": "World"},
)
)
return supervisor.start()
if __name__ == "__main__":
sys.exit(main())
Works with both shell commands and Python callables:
Job(name="work", executable=my_func, env={"API_KEY": "abc123"})
Logs
Every task run is captured to a log file, written as tasks execute in the background so they never block the supervisor. Each process start gets its own file at:
<log_folder>/<task_name>/<run_number>.log
run_number counts process starts, so retries and restarts each get a new
file. The default log folder is /tmp/tinysup/log and can be configured with
the log_folder option:
from tinysupervisor import init_supervisor
supervisor = init_supervisor(log_folder="/var/log/tinysup")
The task output is mirrored to the main console (in addition to the files).
This can be toggled with the stream_logs option.
Each line is prefixed with the task name:
supervisor = init_supervisor(stream_logs=True) # default
The flag can also be overridden per task:
Job(name="too_verbose", command="echo shout", stream_logs=False)
Job(name="quiet", command="echo whisper") # follows the global default
Both shell commands (stdout and stderr) and Python callables (print to
stdout/stderr) are captured. Callable output is captured per-thread, so
concurrent callable tasks will have overlapping logs in the console, where each line carries its task prefix.
But they also write to their own log files.
Observability
The supervisor exposes an HTTP server with three endpoints:
curl http://localhost:8081/health # 200 OK or 400 Unhealthy
curl http://localhost:8081/state # full JSON state of all tasks
curl http://localhost:8081/metrics # Prometheus metrics
The /state endpoint returns JSON with each task's current state, desired state,
run_count, and completed flag, plus the dependency graph:
{
"processes": {
"prepare": {"current": "completed", "desired": "completed", "run_count": 1, "completed": true},
"build": {"current": "completed", "desired": "completed", "run_count": 1, "completed": true}
},
"graph": {
"nodes": ["prepare", "build"],
"edges": [{"from": "prepare", "to": "build", "mode": "completed"}]
}
}
Exit codes
The supervisor's start() returns an exit code:
- 0 — all tasks completed successfully
- 1 — at least one task ended in
FATAL
By default the supervisor exits automatically when all tasks reach a terminal
state (COMPLETED or FATAL). To keep it running (e.g. for long-running services), pass keep_running=True:
supervisor = init_supervisor(keep_running=True)
Verbosity
Control log output via the Verbosity enum or the TINYSUPERVISOR_VERBOSITY
environment variable:
| Level | Behaviour |
|---|---|
silent |
no output |
info |
task start and finish (default) |
debug |
every state transition |
from tinysupervisor import Verbosity
supervisor = init_supervisor(verbosity=Verbosity.DEBUG)
More examples
See the examples/ directory for complete runnable scripts:
simple_dag.py— cron heartbeat with downstream jobssimple_server.py— file-generation cron + HTTP server
Process states
Tasks transition through these states during their lifecycle:
[starting] <-> [backoff]
| | | \ v
| | | \___[FATAL]
| | |
| | [EXITED]
| v /\
| [running] <--> [waiting]
|
\> [stopping] -> [COMPLETED]
For more details see docs/.
Metadata
Release files for tinysupervisor 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tinysupervisor-0.1.0.tar.gz | 21.3 kB | With provenance | Details for tinysupervisor-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tinysupervisor-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for tinysupervisor-0.1.0-py3-none-any.whl |
Total release size: 49.1 kB
Release files / tinysupervisor-0.1.0.tar.gz
| Download URL | tinysupervisor-0.1.0.tar.gz |
|---|---|
| Size | 21.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d3d8f81111a7008bab943615bff1c9cab1487793b056278c281a6bc9e2e20607
|
|
BLAKE2b-256 checksum How to use checksums |
142580f19fa4938a7b8bc0778cf0bb7de68a2a0d0f3ada2cf0be5a3d48a6251b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 28, 2026.
Transparency logRelease files / tinysupervisor-0.1.0-py3-none-any.whl
| Download URL | tinysupervisor-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eac557ebd4a85958111ab49d5423848f408ddfb65f3fefa0a73e8d6c06172633
|
|
BLAKE2b-256 checksum How to use checksums |
9ae656e098098d7288c7d387fcdf792cb9bf0eb934b2fd813149df82c768dad6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 28, 2026.
Transparency logSecurity for tinysupervisor 0.1.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for tinysupervisor-0.1.0.tar.gz
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 Aug 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.