Skip to main content

Playbook Scheduler

A small Python service for scheduling Ansible playbooks and keeping a file-based history of their results. Each execution is stored as JSON and rendered into a single HTML report. No database, web server, or Ansible Python SDK is required; playbooks are started with the ansible-playbook command.

Features

  • YAML configuration with validation of job names, cron expressions, paths, timeout values, and time zone.
  • Manual execution of a configured job or scheduled execution with APScheduler.
  • One uniquely named JSON record per run, including timestamps, duration, command, exit code, standard output, and standard error.
  • Per-host task-result counters and failed task names captured with an Ansible callback plugin.
  • Regenerated HTML report with run history, including skipped runs; long output is shortened in the report but kept in full in the JSON records.
  • Timestamped log of job starts, results, and scheduler events while serve runs.
  • Retention of old run records by age.
  • Parallel execution of different jobs with a configurable limit; a job that is still running is never started a second time, not even by another process.

Requirements

  • Python 3.10 or newer.
  • Ansible installed and ansible-playbook available on PATH.
  • Access to the playbooks, inventory, credentials, and any required Ansible collections or roles.

Installation

git clone https://github.com/andreasdvorak/Ansible_Runner.git
cd Ansible_Runner
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install .

Installing ansible-core in the same environment provides the ansible-playbook executable required to run the example jobs. But you need Python 3.12 or newer.

python -m pip install -e ".[ansible]"

Configure jobs

Edit config/config.yaml to point to your playbooks and inventories. The checked-in example runs Ansible's ping module against localhost:

timezone: Europe/Berlin
runs_directory: ../runs
reports_directory: ../reports
retention_days: 30

jobs:
  - name: local_ping
    cron: "0 2 * * *"
    playbook: examples/playbooks/ping.yml
    inventory: examples/inventory/hosts.ini
    working_directory: ..
    timeout_seconds: 3600
    extra_args: []

Configuration reference

Variable Scope Required / default Description
timezone Global Optional; defaults to UTC IANA time zone used to interpret job schedules, for example Europe/Berlin.
runs_directory Global Optional; defaults to runs Directory for per-run JSON records. Relative paths are resolved from the directory containing the configuration file.
reports_directory Global Optional; defaults to reports Directory for generated HTML reports. Relative paths are resolved from the directory containing the configuration file.
retention_days Global Optional; defaults to 30 Positive number of days to keep run JSON files. Older records are removed during service/report activity.
report_output_lines Global Optional; defaults to 200 Number of trailing lines of standard output and standard error shown per run in the HTML report. Lines longer than 1000 characters are cut. The JSON records always keep the full output.
max_parallel_jobs Global Optional; defaults to 10 Maximum number of different jobs the scheduler runs at the same time. Further due jobs wait for a free slot. Changes take effect after restarting serve.
jobs Global Required; at least one job List of jobs the scheduler can run.
jobs[].name Job Required Unique job identifier used by the CLI and reports. Use letters, numbers, ., _, or -; the first character must be a letter or number.
jobs[].cron Job Required Five-field cron schedule, such as "0 2 * * *". Interpreted in the global timezone.
jobs[].playbook Job Required Path to the Ansible playbook. Relative paths are resolved from working_directory.
jobs[].inventory Job Required Path to the Ansible inventory file or directory. Relative paths are resolved from working_directory.
jobs[].working_directory Job Optional; defaults to the configuration file's directory Working directory for the Ansible process. Relative paths are resolved from the configuration file's directory.
jobs[].timeout_seconds Job Optional; defaults to 3600 Positive maximum runtime for the playbook in seconds. When it is exceeded, ansible-playbook is asked to stop (SIGTERM) so it can end its running tasks, and is killed only if it has not exited after 30 more seconds.
jobs[].extra_args Job Optional; defaults to [] List of additional command-line arguments passed to ansible-playbook. Use one string per argument.
jobs[].ansible_venv Job Optional; defaults to Playbook Scheduler's inherited PATH Path to the Ansible project's virtual environment. Relative paths are resolved from working_directory; the environment must contain ansible-playbook.

The jobs[] notation means a field on each item in the jobs list. Output directories are resolved relative to the configuration file. The job's working_directory is also resolved relative to that file, while relative playbook, inventory, and ansible_venv paths are resolved relative to the working directory.

Use an Ansible project's own virtual environment

Playbook Scheduler and an Ansible project can use separate Python environments. Set working_directory to the Ansible project root and ansible_venv to that project's virtual environment directory:

jobs:
  - name: production_deploy
    cron: "0 2 * * *"
    working_directory: /srv/ansible-project
    playbook: playbooks/deploy.yml
    inventory: inventory/production.ini
    ansible_venv: /srv/ansible-project/.venv
    timeout_seconds: 3600
    extra_args: []

ansible_venv may also be a relative path; it is resolved from working_directory. Playbook Scheduler invokes ansible-playbook from that environment and adds its bin/ directory to the child process's PATH, so Ansible and its installed collections and dependencies are used for that job. On Windows, the executable is resolved under Scripts/. The environment must already contain Ansible (ansible-core) and any required collections. If ansible_venv is omitted, Playbook Scheduler uses ansible-playbook from its inherited PATH.

Validate the configuration and referenced paths before starting:

playbook-scheduler validate

Besides the configuration and the playbook and inventory paths, validate checks that every job finds an executable ansible-playbook and prints its path per job. Jobs with ansible_venv use the executable in that environment; all other jobs use the first ansible-playbook on PATH. The check uses the PATH of the shell that runs validate, so run it as the service account and with the same environment as serve. If an executable is missing, validate exits with code 2.

The supplied configuration points to files under examples/ and can be validated as-is. Replace those paths with your own when adapting the sample.

Use --config to select a different configuration file:

playbook-scheduler --config /etc/playbook-scheduler/config.yaml validate

Run and view reports

Run a job immediately:

playbook-scheduler run local_ping

Generate or refresh the HTML report without starting a playbook:

playbook-scheduler report

The scheduler adds its metrics callback to Ansible's callback plugin search path without overriding callbacks_enabled from ansible.cfg.

The report includes client-side filters for job name, status, and a UTC date range. Filters can be combined; the job-name search is case-insensitive and updates as you type. The report shows the number of matching runs and provides a button to reset all filters. Expand a run's host metrics to see per-host ok, changed, failed, unreachable, skipped, and ignored counters, plus failed task names and their hosts. The metrics callback only includes task and host names for failures; the existing stdout/stderr capture is unchanged.

To keep the report small, it shows only the last report_output_lines lines (default 200) of each run's standard output and standard error, and cuts lines longer than 1000 characters, such as the JSON results Ansible prints with -v. A note above the shortened output names the JSON record that contains the full output.

Runs that were skipped because the job was still running are listed with the status skipped and can be selected in the status filter. They do not count towards the success rate or the average duration.

With the checked-in configuration, outputs are stored in runs/ and reports/index.html. If the output paths are omitted, they default to runs/ and reports/ next to the configuration file. A failed Ansible command is still recorded as a failed run; the manual run command returns a non-zero exit code for that run. Process output is retained in JSON and displayed in the report, so protect these directories as they may contain sensitive information.

Old JSON run records are removed when the service starts, when a job completes, and when a report is generated. Set retention_days to a positive number of days.

Start the scheduler

playbook-scheduler serve

Keep the process running under a service manager such as systemd for unattended scheduling. Run one scheduler instance for a given set of output directories.

Different jobs run in parallel, up to max_parallel_jobs at a time. The same job never runs twice at once: if it is still running when its next fire time arrives, or when it is started with playbook-scheduler run, the new run is skipped and not queued. The skipped run is recorded with the status skipped and appears in the report. run then exits with code 3; the scheduler logs Skipped scheduled run: …. This also applies across processes, because Playbook Scheduler uses lock files in runs_directory/.locks/. Missed fire times within the one-hour misfire grace period are coalesced into a single run.

To stop the scheduler, send SIGTERM (systemctl stop, kill) or press Ctrl+C. Playbook Scheduler then stops scheduling new runs and asks every running ansible-playbook to stop, so Ansible can end its running tasks. Interrupted runs are recorded as failed with the error Interrupted by shutdown., the report is updated, and the process exits. If Ansible does not stop, a second signal kills the remaining ansible-playbook processes.

The scheduler checks the configuration file every 30 seconds and reloads it when it changes: new jobs are added, removed jobs are unscheduled, and changed jobs are rescheduled without a restart. Runs that are already in progress finish with their previous settings. If the changed file is invalid, the error is logged and the previous configuration stays active until the file changes again.

serve writes a log line with timestamp and level to standard error for each job start and result and for scheduler events, for example:

2026-10-03 19:21:00+0200 INFO Starting job slow
2026-10-03 19:22:00+0200 WARNING Skipped scheduled run: Job slow is already running.
2026-10-03 19:22:41+0200 INFO Job slow finished: success after 101.2s (run 6a80…)

Failed runs are logged at level ERROR. Under systemd the log is available with journalctl -u playbook-scheduler.service. There Playbook Scheduler leaves out its own timestamp, because the journal adds one, and passes the level to the journal as syslog priority. journalctl -u playbook-scheduler.service -p err therefore shows only failed runs and other errors, and -p warning adds skipped runs.

Example systemd service

Create a dedicated system account and install Playbook Scheduler in a stable location. For example, save the following as /etc/systemd/system/playbook-scheduler.service:

[Unit]
Description=Playbook Scheduler
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=playbook-scheduler
Group=playbook-scheduler
WorkingDirectory=/opt/playbook-scheduler
ExecStart=/opt/playbook-scheduler/.venv/bin/playbook-scheduler --config /opt/playbook-scheduler/config/config.yaml serve
Restart=on-failure
RestartSec=5
# Send SIGTERM only to Playbook Scheduler, which stops Ansible and records the runs.
KillMode=mixed
TimeoutStopSec=90

[Install]
WantedBy=multi-user.target

KillMode=mixed sends SIGTERM only to Playbook Scheduler instead of to every process of the service at once. Playbook Scheduler then stops Ansible itself and records the interrupted runs. If the service has not stopped after TimeoutStopSec, systemd kills all remaining processes, including Ansible's workers.

Make sure the service account can read the Playbook Scheduler configuration, playbooks, inventory, and credentials, and can write to the configured run and report directories. Set ansible_venv for each job to the Ansible project's virtual environment; it does not need to be activated by systemd. Then load and enable the service:

sudo systemctl daemon-reload
sudo systemctl enable --now playbook-scheduler.service
sudo systemctl status playbook-scheduler.service
sudo journalctl -u playbook-scheduler.service

CLI reference

playbook-scheduler [--config PATH] validate
playbook-scheduler [--config PATH] run JOB_NAME
playbook-scheduler [--config PATH] report
playbook-scheduler [--config PATH] serve
Exit code Meaning
0 Success
1 run: the playbook run failed or timed out
2 Invalid configuration, missing ansible-playbook (validate), or unknown job
3 run: the job is already running

Metadata

Release files for playbook-scheduler 0.1.0

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

Source distribution (sdist)

Source distribution for playbook-scheduler 0.1.0
File Size Uploaded
playbook_scheduler-0.1.0.tar.gz 46.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for playbook-scheduler 0.1.0
File Interpreter ABI Platform
playbook_scheduler-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 82.0 kB

Release files / playbook_scheduler-0.1.0.tar.gz

Download URL playbook_scheduler-0.1.0.tar.gz
Size 46.0 kB
Tags Source
SHA-256 checksum
How to use checksums
35e8994d2b04aa36bae61755dfa33a33084761756340e381c3254c2d9e441916
BLAKE2b-256 checksum
How to use checksums
8d7c8a3b4559b352a225f1579fca1ce03b93661414ec532d9afc6e83291d6654
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / playbook_scheduler-0.1.0-py3-none-any.whl

Download URL playbook_scheduler-0.1.0-py3-none-any.whl
Size 36.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3df1e710f9298d70d35ee69458a067143cfad67ed0daf94d905d77daf59db40
BLAKE2b-256 checksum
How to use checksums
6295b643bf2b537bdf28a746c39b9f48acedd80d4719534881d29bc7b34868d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.3.1

2 release files

This release

0.1.0 This release

2 release files

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