Skip to content

bennybuoy/docket

Repository files navigation

Docket — Deterministic task runner for AI agents

Deterministic task runner for AI agents & systemd timers

Python 3.11+ MIT License Tests PyPI


Replace shell scripts and token-burning LLM cron jobs with a structured, self-documenting CLI. Every task produces JSON output, supports dry-run, and tracks state in SQLite — so agents and schedulers can inspect results without re-running.

Quick demo:

pip install docket-cli
docket configure          # create ~/.docket/config.yaml
docket --json capabilities   # ← agents start here
docket run backup --dry-run  # safe preview, no side effects
docket run backup --async    # fire & forget, get a run_id back

Why Docket?

Problem Docket's answer
Shell scripts don't speak JSON --json on every command, structured output everywhere
Cron jobs fail silently SQLite state tracking, completion hooks, native notifications
Agents burn tokens on simple ops Deterministic tasks — no LLM needed, --json contract for agents
Long-running tasks block --async returns a run_id, docket wait / docket logs to follow up
No way to preview before running --dry-run on every task, zero side effects

Install

pip install docket-cli

Dev setup:

git clone https://github.com/bennybuoy/docket.git
cd docket && 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 also wrap hbackup through tool: hbackup, or create a .tar.zst archive and upload it to Google Drive with gog through tool: gog.

Config keys: source, remote, tool (rclone, rsync, hbackup, or gog), extra_args, idle_timeout_seconds, hbackup_bin, hbackup_command, output, excludes, gog_bin, account, parent, archive_name, and mime_type.

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 first-class hbackup orchestration, use docket run hbackup.

Google Drive upload with gog backend:

tasks:
  backup:
    tool: gog
    source: /home/openclaw/.openclaw
    account: benkamholtz@gmail.com
    parent: 1okGC-kRTFSzIGhkmfnxO8_zO0if9El1q  # Drive folder ID
    archive_name: openclaw-backup.tar.zst
    excludes:
      - .openclaw/workspace-*
      - .openclaw/github-backup
      - .openclaw/cache
    idle_timeout_seconds: 1800
    required_env: [GOG_KEYRING_PASSWORD]

tool: gog creates a temporary tar --zstd archive from source, then runs gog drive upload --json --no-input --name <archive_name> --parent <parent>. excludes are passed to tar --exclude, so patterns should be written relative to the parent of source (for example .openclaw/workspace-* when source: /home/openclaw/.openclaw). It preserves Docket's run history, async receipts, hooks, dry-run preview, and activity-based idle-timeout behaviour.

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

About

Deterministic task runner for AI agents and systemd timers

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages