Skip to main content

Docket

Deterministic task runner for AI agents and systemd timers.

Python 3.11+ MIT License Tests

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 — --json flag for machine / agent consumption
  • ✅ Dry-run support — --dry-run on every task, no side effects
  • ✅ Configurable retry with exponential backoff — per-task retry and backoff_base
  • ✅ Lock files — prevent concurrent runs of the same task (--force to bypass)
  • ✅ Notification hooks — on_success / on_failure / on_complete shell commands
  • ✅ systemd timer generation — docket install writes .service + .timer units
  • ✅ Config editing — docket configure opens config in $EDITOR with validation
  • ✅ Health self-check — docket doctor verifies 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 capabilities describes 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 strings
  • retry: non-negative integer
  • backoff_base: positive number
  • on_success, on_failure, on_complete: string or list of strings
  • notify: mapping with on, discord, and/or telegram
  • inputs, outputs: mapping of string keys to string descriptions
  • required_env: list of strings
  • network, destructive: booleans

Built-in tasks get extra validation:

  • backup.tool: rclone, rsync, or hbackup
  • backup.source, backup.remote: non-empty strings when present
  • backup.extra_args: list of strings
  • backup.idle_timeout_seconds: non-negative number or null
  • backup.hbackup_command: backup or auto
  • backup.hbackup_bin, backup.output: non-empty strings
  • backup.excludes: list of strings
  • hbackup.action: backup, auto, list, upload, or setup-drive
  • hbackup.hbackup_bin, hbackup.output, hbackup.archive, hbackup.destination, hbackup.drive_remote, hbackup.drive_folder: non-empty strings
  • hbackup.excludes, hbackup.extra_args: list of strings
  • hbackup.drive: boolean
  • hbackup.idle_timeout_seconds: non-negative number or null
  • health-check.checks: list containing disk, memory, and/or systemd-units
  • health-check.*_threshold_pct: number from 0 to 100
  • health-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 strings
  • args, dry_run_args: list of strings
  • dry_run_command: string or list of strings
  • output_format: text, json, jsonl, or auto
  • success_exit_codes: list of integers
  • env: mapping with string keys
  • cwd: non-empty string
  • timeout_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

MIT

Metadata

Release files for docket-cli 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for docket-cli 0.2.2
File Size Uploaded Provenance
docket_cli-0.2.2.tar.gz 97.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for docket-cli 0.2.2
File Interpreter ABI Platform
docket_cli-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 163.8 kB

Release files / docket_cli-0.2.2.tar.gz

Download URL docket_cli-0.2.2.tar.gz
Size 97.9 kB
Tags Source
SHA-256 checksum
How to use checksums
acf1921e871d1ddcd2deb2274415d4b7668ed16d076c9a6a5ccab2b520715795
BLAKE2b-256 checksum
How to use checksums
418c3dcc5cd41435aa82a9fd459676191c42902d72b87819363072aed8ba4846
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release files / docket_cli-0.2.2-py3-none-any.whl

Download URL docket_cli-0.2.2-py3-none-any.whl
Size 65.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
81ec32dd6cd77269a939347ccc0867dd1a03a93768ee58d76c6c7a71611d4c18
BLAKE2b-256 checksum
How to use checksums
5107cf2cd4ef5b6a920ab79316846ccbee9c76dddcab530a5919f45b93a6074c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Security for docket-cli 0.2.2

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page