Skip to main content

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.

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 hookson_success / on_failure / on_complete shell commands
  • systemd timer generationdocket install writes .service + .timer units
  • Config editingdocket configure opens config in $EDITOR with validation
  • Health self-checkdocket 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-discoverydocket --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

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

docket_cli-0.2.2.tar.gz (97.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

docket_cli-0.2.2-py3-none-any.whl (65.9 kB view details)

Uploaded Python 3

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

Hashes for docket_cli-0.2.2.tar.gz
Algorithm Hash digest
SHA256 acf1921e871d1ddcd2deb2274415d4b7668ed16d076c9a6a5ccab2b520715795
MD5 8daa81b9e72be46d9d4a6b4970d7c525
BLAKE2b-256 418c3dcc5cd41435aa82a9fd459676191c42902d72b87819363072aed8ba4846

See more details on using hashes here.

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

Hashes for docket_cli-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 81ec32dd6cd77269a939347ccc0867dd1a03a93768ee58d76c6c7a71611d4c18
MD5 6e437e53f5190301da60b763dba903a6
BLAKE2b-256 5107cf2cd4ef5b6a920ab79316846ccbee9c76dddcab530a5919f45b93a6074c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page