Deterministic task runner for AI agents — replaces shell scripts and token-burning LLM cron jobs
Project description
Docket
Deterministic task runner for AI agents and systemd timers.
Docket gives AI agents and systemd timers a structured, self-documenting entry point for deterministic scheduled tasks — replacing shell scripts and token-burning LLM cron jobs for ops work that doesn't need reasoning. Every task produces structured JSON output, supports dry-run previews, and tracks state in SQLite so agents and schedulers can inspect results without re-running.
Features
- ✅ Deterministic task runner — a clean CLI for ops work that doesn't need LLM reasoning
- ✅ Structured JSON output —
--jsonflag for machine / agent consumption - ✅ Dry-run support —
--dry-runon every task, no side effects - ✅ Configurable retry with exponential backoff — per-task
retryandbackoff_base - ✅ Lock files — prevent concurrent runs of the same task (
--forceto bypass) - ✅ Notification hooks —
on_success/on_failure/on_completeshell commands - ✅ systemd timer generation —
docket installwrites.service+.timerunits - ✅ Config editing —
docket configureopens config in$EDITORwith validation - ✅ Health self-check —
docket doctorverifies your installation - ✅ Drop-in task discovery — entry points +
~/.docket/tasks/directory - ✅ SQLite-backed state tracking — last run, exit code, and summary per task
- ✅ Agent self-discovery —
docket --json capabilitiesdescribes commands, tasks, inputs, outputs, and safety flags - ✅ Async run receipts — long tasks return a durable
run_id, logs path, wait/status commands, and polling hint - ✅ Command task adapter — wrap Rust, Go, Bash, Node, or any executable as an agent-visible task
Installation
# Install from PyPI
pip install docket-cli
# Install for development
pip install -e ".[dev]"
Quick start
1. Create a config file:
docket configure
Or manually create ~/.docket/config.yaml:
tasks:
backup:
source: /home/user/data
remote: remote:backup
tool: rclone
schedule: "0 2 * * *"
retry: 3
backoff_base: 2.0
on_success: "notify-send 'Backup succeeded'"
on_failure: "notify-send 'Backup failed'"
health-check:
checks: [disk, memory, systemd-units]
disk_threshold_pct: 85
memory_threshold_pct: 90
systemd_units: [nginx.service, postgresql.service]
2. Run a task:
docket --json capabilities # discover commands and task contracts
docket run hbackup --dry-run # preview Hermes/OpenClaw backup
docket run hbackup --async # start hbackup in background
docket run backup --dry-run # preview
docket run backup # execute for real
docket run backup --async # start in background and return a run_id
3. Check status:
docket status
docket status backup # specific task
docket runs backup # recent run history
docket wait <run_id> # block until an async run finishes
docket logs <run_id> # show captured stdout/stderr
docket logs <run_id> --follow # stream captured output until completion
docket events <run_id> # show structured JSONL events
docket events <run_id> --follow # stream structured events until completion
docket cancel <run_id> # terminate a running async task
Commands
| Command | Description |
|---|---|
docket capabilities |
Show agent-facing command and task metadata |
docket list |
List all discovered tasks and their last run status |
docket new-task <task> |
Scaffold a drop-in task in tasks_dir |
docket run <task> |
Execute a task (--dry-run to preview, --force to bypass guards) |
docket run <task> --async |
Start a task in the background and return a durable run ID |
docket status [<task>] |
Show last run status for all tasks or a specific task |
docket status --run-id <id> |
Show a specific historical run |
docket runs [<task>] |
List recent historical runs |
docket wait <run_id> |
Wait for a running task to finish |
docket logs <run_id> |
Show captured stdout/stderr for an async run |
docket logs <run_id> --follow |
Follow captured async output until the run finishes |
docket events <run_id> |
Show structured JSONL events recorded for a run |
docket events <run_id> --follow |
Follow structured events until the run finishes |
docket cancel <run_id> |
Cancel a running async task |
docket describe <task> |
Show a task's description and resolved configuration |
docket install [<task>] |
Generate systemd timer + service units for scheduled tasks (--list to preview) |
docket configure |
Open config in $EDITOR with YAML validation on save |
docket doctor |
Self-diagnostic: checks config, state DB, and task discovery |
All commands support --json for structured output and -v / -vv for logging.
Agent discovery
Agents should begin with the self-describing contract:
docket --json capabilities
The response includes the Docket version, agent_contract_version, configured
state/log/task directories, available commands, and every discovered task with
its description, resolved config, inputs, outputs, schema, required
environment variables, and safety flags:
{
"tool": "docket",
"agent_contract_version": 1,
"commands": [{"name": "run <task> --async", "json": true}],
"tasks": [
{
"name": "hbackup",
"description": "Back up Hermes/OpenClaw state with hbackup",
"inputs": {"action": "hbackup action: backup, auto, list, upload, or setup-drive"},
"outputs": {"archive_path": "Archive path reported by hbackup"},
"schema": {},
"required_env": [],
"network": true,
"destructive": false
}
]
}
Use docket --json describe <task> for a focused view before running an
unfamiliar task.
Agent run receipts
docket run <task> --async --json returns immediately with a durable run_id,
the child process PID, and the exact status/wait commands an agent can call next.
{
"ok": true,
"status": "running",
"task": "backup",
"run_id": "20260427T013000000000Z-ab12cd34",
"pid": 12345,
"log_path": "/home/user/.docket/logs/20260427T013000000000Z-ab12cd34.log",
"next_check_after_seconds": 5,
"status_command": "docket --json status --run-id 20260427T013000000000Z-ab12cd34",
"wait_command": "docket --json wait 20260427T013000000000Z-ab12cd34",
"logs_command": "docket logs 20260427T013000000000Z-ab12cd34"
}
Completed runs are stored in SQLite history, not just the latest task state, so
agents can inspect past outcomes with docket runs --json.
Async child output is captured under logs_dir, and agents can read it with
docket logs <run_id> without needing to know the filesystem layout. Use
docket logs <run_id> --follow for live log tailing, docket events <run_id> --json for parsed JSONL events, and docket events <run_id> --follow to watch
structured command-task events as they arrive.
Configuration
Configuration lives in ~/.docket/config.yaml. The full schema:
# Top-level settings
state_dir: ~/.docket/state.db # SQLite database for run history
tasks_dir: ~/.docket/tasks # Drop-in directory for custom task .py files
logs_dir: ~/.docket/logs # Captured logs for async task runs
# Per-task configuration under "tasks"
tasks:
backup:
source: /data/backup # Local path to back up
remote: remote:backup # rclone remote destination
tool: rclone # "rclone", "rsync", or "hbackup"
extra_args: [] # Additional CLI args for the backup tool
idle_timeout_seconds: 1800 # Kill if no output/progress for this many seconds
schedule: "0 2 * * *" # Cron schedule (for docket install)
retry: 3 # Max retry attempts on failure
backoff_base: 2.0 # Exponential backoff base (seconds)
on_success: "notify-send 'Backup done'" # Shell command(s) on success
on_failure: "notify-send 'Backup failed'" # Shell command(s) on failure
on_complete: "curl -X POST https://example.test/docket" # Always runs
notify:
on: [success, failure] # Default is [failure] when omitted
discord:
webhook_url_env: DISCORD_WEBHOOK_URL
telegram:
chat: "8223639759"
bot_token_env: TELEGRAM_BOT_TOKEN
docket_bin: docket # Override docket binary path for systemd units
health-check:
checks: [disk, memory, systemd-units] # Which checks to run
disk_threshold_pct: 90 # Fail if disk usage exceeds this %
memory_threshold_pct: 90 # Fail if memory usage exceeds this %
systemd_units: [] # Units that must be active
schedule: "*/15 * * * *" # Every 15 minutes
Per-task config keys
| Key | Type | Default | Description |
|---|---|---|---|
schedule |
string | — | Cron expression for systemd timer generation |
retry |
int | 0 |
Max retry attempts when task fails |
backoff_base |
float | 2.0 |
Base for exponential backoff (wait = backoff_base ** attempt) |
on_success |
string or list | — | Shell command(s) to run on task success |
on_failure |
string or list | — | Shell command(s) to run on task failure |
on_complete |
string or list | — | Shell command(s) to run after any terminal result |
notify |
mapping | — | Native Discord/Telegram notifications for task completion |
docket_bin |
string | docket |
Path to docket binary for generated systemd units |
All other keys are passed directly to the task's run() / dry_run() as the config dict.
Config validation
Docket validates shared config before commands run. Top-level state_dir,
tasks_dir, and logs_dir must be strings; tasks must be a mapping; task
configs must be mappings.
Shared per-task keys are typed:
schedule,description,docket_bin: non-empty stringsretry: non-negative integerbackoff_base: positive numberon_success,on_failure,on_complete: string or list of stringsnotify: mapping withon,discord, and/ortelegraminputs,outputs: mapping of string keys to string descriptionsrequired_env: list of stringsnetwork,destructive: booleans
Built-in tasks get extra validation:
backup.tool:rclone,rsync, orhbackupbackup.source,backup.remote: non-empty strings when presentbackup.extra_args: list of stringsbackup.idle_timeout_seconds: non-negative number ornullbackup.hbackup_command:backuporautobackup.hbackup_bin,backup.output: non-empty stringsbackup.excludes: list of stringshbackup.action:backup,auto,list,upload, orsetup-drivehbackup.hbackup_bin,hbackup.output,hbackup.archive,hbackup.destination,hbackup.drive_remote,hbackup.drive_folder: non-empty stringshbackup.excludes,hbackup.extra_args: list of stringshbackup.drive: booleanhbackup.idle_timeout_seconds: non-negative number ornullhealth-check.checks: list containingdisk,memory, and/orsystemd-unitshealth-check.*_threshold_pct: number from0to100health-check.systemd_units: list of strings
Unknown keys on custom tasks are allowed so task plugins can define their own configuration contract.
Command tasks (type: command) get extra validation:
command: string or list of stringsargs,dry_run_args: list of stringsdry_run_command: string or list of stringsoutput_format:text,json,jsonl, orautosuccess_exit_codes: list of integersenv: mapping with string keyscwd: non-empty stringtimeout_seconds: positive number
Writing custom tasks
Create a drop-in task scaffold:
docket --json new-task fetch-invoices --description "Fetch invoices from the API"
Then edit the generated file in tasks_dir, or create a Python class extending
BaseTask yourself:
from docket.task import BaseTask, TaskResult
class MyTask(BaseTask):
name = "my-task"
description = "Does something useful"
inputs = {"api_url": "API endpoint to fetch"}
outputs = {"records_fetched": "Number of records fetched"}
schema = {"api_url": {"type": "string", "required": True}}
required_env = ["API_TOKEN"]
network = True
destructive = False
def run(self, config: dict) -> TaskResult:
# ... actual work ...
return TaskResult(ok=True, summary="Done", details={"records_fetched": 12})
def dry_run(self, config: dict) -> TaskResult:
return TaskResult(ok=True, summary="Would do something", dry_run=True)
Register via entry point in pyproject.toml:
[project.entry-points."docket.tasks"]
my-task = "my_package.tasks:MyTask"
Or drop a .py file into ~/.docket/tasks/.
The metadata fields are optional but strongly recommended for agent use:
| Field | Meaning |
|---|---|
inputs |
Config keys or external inputs the task expects |
outputs |
Machine-readable values the task returns in TaskResult.details |
schema |
Optional config validation schema checked before execution |
required_env |
Environment variables needed at runtime |
network |
Whether the task calls external services |
destructive |
Whether the task can delete, overwrite, charge money, or mutate important state |
See docs/writing-tasks.md for the full guide.
Command tasks
Use type: command when the implementation should live in Rust, Go, Bash,
Node, or any existing executable:
tasks:
fetch-invoices:
type: command
description: Fetch invoices with the Rust importer
command: invoice-fetcher
args: ["--account", "main", "--jsonl"]
dry_run_args: ["--account", "main", "--dry-run", "--json"]
output_format: jsonl
required_env: [INVOICE_API_TOKEN]
inputs:
account: Account slug to fetch
outputs:
events: JSONL progress/result events emitted by the command
network: true
destructive: false
For output_format: json, Docket parses stdout into details.json.
For output_format: jsonl, each stdout line must be a JSON object; Docket
stores parsed events in details.events, details.event_count, and
details.last_event. Raw stdout/stderr are still preserved in the result and,
for async runs, the Docket child output is captured under logs_dir.
JSONL events are also promoted into SQLite and can be queried independently:
docket --json events <run_id>
docket events <run_id> --follow
Each event record includes run_id, task_name, event_index, timestamp,
event_type, and the parsed event object. Async command tasks stream JSONL
events into this table while the command is still running.
If dry_run_args or dry_run_command is omitted, docket run <task> --dry-run
returns a safe preview without executing the command.
Completion hooks
on_complete runs after every task result is recorded, regardless of success or
failure. on_success and on_failure still run only for matching outcomes.
Hooks receive structured environment variables:
| Variable | Meaning |
|---|---|
DOCKET_RUN_ID |
Durable run ID |
DOCKET_TASK_NAME |
Task name |
DOCKET_STATUS |
succeeded, failed, cancelled, etc. |
DOCKET_EXIT_CODE |
Final Docket exit code |
DOCKET_SUMMARY |
One-line result summary |
DOCKET_DETAILS_JSON |
JSON-encoded result details |
DOCKET_LOG_PATH |
Async log path, when available |
This is the lightweight way to ping an agent, webhook, or monitor when a long job finishes.
Native notifications
For common agent channels, use notify instead of shelling out to curl.
Docket currently supports Discord webhooks and Telegram bot messages:
tasks:
vitals-morning:
notify:
on: [success, failure] # success, failure, or complete
message: "{task} {status}: {summary} ({run_id})"
discord:
webhook_url_env: DISCORD_WEBHOOK_URL
username: Docket
telegram:
chat: "8223639759"
bot_token_env: TELEGRAM_BOT_TOKEN
When notify.on is omitted, notifications default to failures only. Prefer
*_env settings so secrets stay in the environment instead of YAML. Delivery
results are included in --json output under hooks as notify:discord and
notify:telegram entries.
Built-in tasks
hbackup
First-class integration for hbackup, the Hermes/OpenClaw backup and restore CLI.
tasks:
hbackup:
action: auto # backup, auto, list, upload, setup-drive
hbackup_bin: hbackup
idle_timeout_seconds: 1800
schedule: "0 3 * * *"
retry: 1
on_complete: "curl -X POST https://example.test/docket-complete"
Agent workflow:
docket --json describe hbackup
docket --json run hbackup --dry-run
docket --json run hbackup --async --force
docket logs <run_id> --follow
docket --json status --run-id <run_id>
action: auto lets hbackup create and upload using its own config while Docket
provides the agent contract: run IDs, async receipts, logs, retries, history,
cancellation, completion hooks, and task discovery.
backup
Syncs a local directory to a remote destination using rclone (default) or
rsync. It can still wrap hbackup through tool: hbackup, but the
preferred Hermes/OpenClaw launch path is the first-class hbackup task.
Config keys: source, remote, tool (rclone, rsync, or hbackup),
extra_args, idle_timeout_seconds, hbackup_bin, hbackup_command,
output, and excludes.
tasks:
hbackup:
action: auto
schedule: "0 3 * * *"
retry: 1
on_complete: "notify-send 'hbackup finished'"
backup:
source: /home/user/data
remote: remote:backup
tool: rclone
idle_timeout_seconds: 1800
idle_timeout_seconds is activity-based, not a hard job duration limit. Large
backups can run for hours as long as rclone or rsync continues to emit output
or progress. Set it to 0 or null to disable idle timeout handling.
Compatibility hbackup backend:
tasks:
hbackup:
action: auto # backup, auto, list, upload, setup-drive
hbackup_bin: hbackup # hbackup binary path or command name
idle_timeout_seconds: 1800
schedule: "0 3 * * *"
retry: 1
on_complete: "curl -X POST https://example.test/docket"
backup:
tool: hbackup
hbackup_command: backup # backup or auto
output: ~/backups/openclaw-hermes.tar.zst
excludes: [target, .git]
docket run backup --dry-run maps to hbackup backup --dry-run.
For new installs, prefer docket run hbackup.
health-check
Runs system health checks: disk usage, memory usage, and systemd unit status.
Config keys: checks (list of disk, memory, systemd-units), disk_threshold_pct, memory_threshold_pct, systemd_units.
tasks:
health-check:
checks: [disk, memory, systemd-units]
disk_threshold_pct: 85
memory_threshold_pct: 90
systemd_units: [nginx.service]
Exit codes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Failure (task error, lock contention, config error) |
2 |
Dry-run completed (no side effects) |
Architecture
CLI (click)
│
├─► Registry ── discovers ──► BaseTask subclasses
│ (entry points + ~/.docket/tasks/)
│
├─► Config (YAML loader with defaults)
│
├─► Task.run() / Task.dry_run()
│ ├─► Retry (exponential backoff)
│ ├─► LockFile (prevent concurrent runs)
│ └─► Hooks (on_success / on_failure / on_complete shell commands)
│
└─► StateStore (SQLite)
└─ latest task state + immutable run history
Key modules:
| Module | Role |
|---|---|
docket.cli |
Click CLI — all commands |
docket.task |
BaseTask ABC + TaskResult dataclass |
docket.registry |
Task discovery via entry points and drop-in directory |
docket.config |
YAML config loader with path expansion and defaults |
docket.state |
SQLite-backed state store (latest state and run history) |
docket.lockfile |
Cross-platform lock files to prevent concurrent runs |
docket.retry |
Exponential backoff retry logic |
docket.hooks |
Notification hooks (on_success / on_failure / on_complete) |
docket.systemd |
systemd unit generation and cron↔OnCalendar conversion |
docket.logging |
Dual JSON / human-readable log setup |
Development
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
python -m pytest tests/ -q
# Lint
ruff check .
# Build
python -m build
License
Project details
Release history Release notifications | RSS feed
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 docket_cli-0.2.2.tar.gz.
File metadata
- Download URL: docket_cli-0.2.2.tar.gz
- Upload date:
- Size: 97.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acf1921e871d1ddcd2deb2274415d4b7668ed16d076c9a6a5ccab2b520715795
|
|
| MD5 |
8daa81b9e72be46d9d4a6b4970d7c525
|
|
| BLAKE2b-256 |
418c3dcc5cd41435aa82a9fd459676191c42902d72b87819363072aed8ba4846
|
File details
Details for the file docket_cli-0.2.2-py3-none-any.whl.
File metadata
- Download URL: docket_cli-0.2.2-py3-none-any.whl
- Upload date:
- Size: 65.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81ec32dd6cd77269a939347ccc0867dd1a03a93768ee58d76c6c7a71611d4c18
|
|
| MD5 |
6e437e53f5190301da60b763dba903a6
|
|
| BLAKE2b-256 |
5107cf2cd4ef5b6a920ab79316846ccbee9c76dddcab530a5919f45b93a6074c
|