How-to guides¶
Run jobs on an Airflow worker or a remote SSH host, with configuration in Python
or airflow-config YAML.
Execution |
Inline Python |
|
|---|---|---|
Local worker |
||
SSH host |
How to run a local job in Python¶
Install airflow-supervisor[airflow] and supervisor in your Airflow 2
environment. Use airflow-supervisor[airflow3] for Airflow 3.
Run every lifecycle task on the same host, with access to the same working directory and supervisord endpoint. Use a single worker host or route this DAG to a queue served by that host. Shared storage alone does not make a local supervisord instance available on another worker.
Save this DAG in your DAG folder. Replace /bin/sleep 5 with your job’s command,
and choose a writable working directory and an unused port:
from datetime import datetime, timezone
from airflow import DAG
from airflow_supervisor import ProgramConfiguration, Supervisor, SupervisorAirflowConfiguration
with DAG(
dag_id="local-supervisor",
schedule="@daily",
start_date=datetime(2025, 1, 1, tzinfo=timezone.utc),
catchup=False,
) as dag:
supervisor = Supervisor(
dag=dag,
cfg=SupervisorAirflowConfiguration(
working_dir="/var/tmp/local-supervisor",
port="127.0.0.1:9001",
program={"batch": ProgramConfiguration(command="/bin/sleep 5")},
),
)
Keep Supervisor construction inside the DAG context. Run
airflow tasks list local-supervisor to confirm that Airflow loaded its lifecycle
tasks, then trigger local-supervisor.
How to run a local job with airflow-config¶
Use the same worker placement and package setup as the
local Python guide, and install
airflow-config in the DAG parsing environment.
Create config/local_supervisor.yaml beside your DAG loader:
# @package _global_
_target_: airflow_config.Configuration
dags:
local-supervisor:
schedule: "@daily"
start_date: "2025-01-01"
catchup: false
tasks:
run:
_target_: airflow_supervisor.SupervisorTask
cfg:
working_dir: /var/tmp/local-supervisor
port: "127.0.0.1:9001"
program:
batch:
command: /bin/sleep 5
Save local_supervisor.py in the DAG folder:
"""Generate Airflow DAGs from the local supervisor configuration."""
from airflow_config import load_config
config = load_config("config", "local_supervisor")
config.generate_in_mem()
Run airflow tasks list local-supervisor, then trigger local-supervisor.
Deploy either this loader or the inline Python DAG for that DAG ID.
How to run a job over SSH in Python¶
Install airflow-supervisor[airflow] in Airflow 2, or
airflow-supervisor[airflow3] in Airflow 3. These extras include the SSH provider.
On the remote host, install supervisor-pydantic and supervisor:
pip install supervisor-pydantic supervisor
Create an Airflow SSH connection named supervisor-host for jobs.example.com.
Its account needs permission to run the job and write the remote working
directory. Ensure supervisord and _supervisor_convenience are on the remote
non-interactive shell’s PATH. For a remote virtual environment, set
command_prefix to its activation command, such as
source /opt/supervisor-venv/bin/activate.
Allow Airflow workers to reach the remote supervisord HTTP/XML-RPC endpoint on
port 9001. Restrict access to those workers. Set SUPERVISOR_RPC_PASSWORD in the
DAG parsing environment and worker environments. This password authenticates
XML-RPC requests separately from the SSH connection.
Save this DAG, replacing the host, working directory, and command:
import os
from datetime import datetime, timezone
from airflow import DAG
from airflow_pydantic import SSHOperatorArgs
from airflow_supervisor import ProgramConfiguration, SupervisorSSH, SupervisorSSHAirflowConfiguration
with DAG(
dag_id="ssh-supervisor",
schedule="@daily",
start_date=datetime(2025, 1, 1, tzinfo=timezone.utc),
catchup=False,
) as dag:
supervisor = SupervisorSSH(
dag=dag,
cfg=SupervisorSSHAirflowConfiguration(
working_dir="/var/tmp/ssh-supervisor",
host="jobs.example.com",
port="*:9001",
username="airflow",
password=os.environ["SUPERVISOR_RPC_PASSWORD"],
ssh_operator_args=SSHOperatorArgs(ssh_conn_id="supervisor-host"),
program={"batch": ProgramConfiguration(command="/bin/sleep 5")},
),
)
Set cfg.host to the XML-RPC hostname and the SSH connection’s host to the same
machine. cfg.port is the supervisord listen address, not the SSH port.
Run airflow tasks list ssh-supervisor, then trigger ssh-supervisor.
How to run a job over SSH with airflow-config¶
Complete the remote package, SSH connection, XML-RPC network, and password
setup from the SSH Python guide.
Install airflow-config in the DAG parsing environment.
Create config/ssh_supervisor.yaml beside your DAG loader:
# @package _global_
_target_: airflow_config.Configuration
dags:
ssh-supervisor:
schedule: "@daily"
start_date: "2025-01-01"
catchup: false
tasks:
run:
_target_: airflow_supervisor.SupervisorSSHTask
cfg:
working_dir: /var/tmp/ssh-supervisor
host: jobs.example.com
port: "*:9001"
username: airflow
password: ${oc.env:SUPERVISOR_RPC_PASSWORD}
ssh_operator_args:
ssh_conn_id: supervisor-host
program:
batch:
command: /bin/sleep 5
If the remote tools are in a virtual environment, add
command_prefix: source /opt/supervisor-venv/bin/activate under cfg.
Save ssh_supervisor.py in the DAG folder:
"""Generate Airflow DAGs from the SSH supervisor configuration."""
from airflow_config import load_config
config = load_config("config", "ssh_supervisor")
config.generate_in_mem()
Run airflow tasks list ssh-supervisor, then trigger ssh-supervisor.
Deploy either this loader or the inline Python DAG for that DAG ID.
How to limit monitoring¶
Set the polling interval, sensor timeout, and workload end condition under cfg
in either local or SSH YAML configuration:
check_interval: 00:00:10
check_timeout: 08:00:00
runtime: 04:00:00
maxretrigger: 3
In Python, set check_interval=timedelta(seconds=10),
check_timeout=timedelta(hours=8), runtime=timedelta(hours=4), and
maxretrigger=3 on the configuration model, importing timedelta from
datetime. Use check_timeout for the sensor’s time limit and runtime or
endtime to end monitoring through the lifecycle’s stop branch. See the
timing reference for defaults and
reference dates.
How to choose lifecycle defaults¶
Use a preset when several jobs need the same monitoring and cleanup settings:
from airflow_supervisor import SupervisorAirflowConfiguration
cfg = SupervisorAirflowConfiguration(
preset="always_on",
working_dir="/var/tmp/service",
program={"service": {"command": "/opt/service/bin/run"}},
)
always_on retains the service and its directory after a seven-day monitoring
window. Use daily for a one-day window that stops and cleans up on success, or
half_day for a 12-hour window ending by 20:00 that stops the service but keeps
its files. These windows are relative to the configured reference date.
For an airflow-config configuration, set the same field in the Supervisor model:
_target_: airflow_supervisor.SupervisorAirflowConfiguration
preset: half_day
working_dir: /var/tmp/report
program:
report:
command: /opt/report/bin/run
check_interval: 15
Explicit settings override preset values. Both local and
SupervisorSSHAirflowConfiguration models accept presets. Refer to the
preset reference for restart flags,
timeouts, and cleanup settings; choose the DAG schedule separately.
How to keep a service running between DAG runs¶
Set these fields on either configuration model, or under cfg in YAML:
stop_on_exit: false
cleanup: false
restart_on_initial: true
restart_on_retrigger: true
runtime: 04:00:00
Use the same working directory and endpoint on each run. Set runtime or
endtime to end monitoring while the service remains active. A running service
with no end condition continues to occupy the monitoring task until its sensor
timeout. See the timing reference for
the reference date used to calculate those limits.
How to select a host with airflow-balancer¶
Pass a selected airflow-pydantic Host or a HostQuery of kind select as
SupervisorSSHTask.host, outside cfg. Pass a Port or PortQuery as the
task’s port to select the XML-RPC port. In inline Python, pass resolved Host
and Port objects to SupervisorSSH.
Keep the program configuration under cfg. If the selected host defines a pool
and cfg.pool is unset, its pool is assigned to the lifecycle tasks.
How to chain the lifecycle with other tasks¶
In Python, connect existing Airflow tasks to the Supervisor or SupervisorSSH
object:
prepare >> supervisor >> publish
In airflow-config, use the supervisor task’s mapping key in dependencies:
tasks:
prepare:
_target_: airflow_pydantic.BashTask
bash_command: echo prepare
run:
_target_: airflow_supervisor.SupervisorTask
dependencies: [prepare]
cfg:
working_dir: /var/tmp/chained-supervisor
port: "127.0.0.1:9001"
program:
batch:
command: /bin/sleep 5
publish:
_target_: airflow_pydantic.BashTask
bash_command: echo publish
dependencies: [run]
Upstream tasks precede configuration; downstream tasks follow unconfiguration.
Keep stop and cleanup enabled if downstream tasks should run with the default
all_success trigger rule. Disabling cleanup creates a skipped boundary task.
How to configure generated tasks with subtasks¶
Keep the Supervisor lifecycle as one outer task so normal tasks can run before
configuration and after cleanup. Put internal task options under subtasks:
# @package _global_
supervisor:
_target_: airflow_supervisor.SupervisorAirflowConfiguration
working_dir: /tmp/ordered-services
runtime: 28800
reference_date: start_date
program:
A: {command: /opt/services/a}
B: {command: /opt/services/b}
C: {command: /opt/services/c}
program_lifecycle:
C:
required: false
max_consecutive_failures: 3
dags:
ordered-services:
tasks:
prepare:
_target_: airflow_pydantic.BashTask
bash_command: echo prepare
service:
_target_: airflow_supervisor.SupervisorTask
cfg: ${supervisor}
dependencies: [prepare]
subtasks:
start-A:
_target_: airflow_pydantic.PythonTask
retries: 2
retry_delay: 15
wait-after-A:
_target_: airflow_pydantic.WaitSensor
time_to_wait: 600
deferrable: true
dependencies: [start-A]
start-B:
_target_: airflow_pydantic.PythonTask
dependencies: [wait-after-A]
retries: 3
start-C:
_target_: airflow_pydantic.PythonTask
dependencies: [start-B]
retries: 2
stop-C:
_target_: airflow_pydantic.PythonTask
dependencies: [shutdown]
stop-B:
_target_: airflow_pydantic.PythonTask
dependencies: [stop-C]
stop-A:
_target_: airflow_pydantic.PythonTask
dependencies: [stop-B]
publish:
_target_: airflow_pydantic.BashTask
bash_command: echo publish
dependencies: [service]
start-A, start-B, and start-C configure existing tasks. Their generated
callables remain in place. wait-after-A adds a real Airflow sensor between A
and B; it waits ten minutes after the sensor starts, following A’s successful
start task. This differs from program_lifecycle.start_delay, measured from the
Supervisor process start time.
The shutdown gate selects terminal cleanup. The explicit stop dependencies
choose C, B, A; change those dependencies to choose another order. The
cross-retrigger budget for optional C remains separate from Airflow task retries.
If you omit the budget configuration, all programs are required by default.
For SSH, use SupervisorSSHTask and SupervisorSSHAirflowConfiguration with the
usual SSH settings. Program operations still use PythonTask because they call
XML-RPC. Configure SSH-specific daemon steps using SSHTask:
subtasks:
configure-supervisor:
_target_: airflow_pydantic.SSHTask
retries: 2
cmd_timeout: 120
For inline Python, pass the same task models directly:
from airflow_pydantic import PythonTask, WaitSensor
service = Supervisor(
dag=dag,
cfg=cfg,
subtasks={
"start-A": PythonTask(retries=2),
"wait-after-A": WaitSensor(time_to_wait=600, deferrable=True, dependencies=["start-A"]),
"start-B": PythonTask(dependencies=["wait-after-A"], retries=3),
"start-C": PythonTask(dependencies=["start-B"]),
},
)
How to use convenience program policies¶
Set program_lifecycle to create separate start, delay, and stop tasks. This
example starts A, waits ten minutes from A’s process start, starts B, then C.
Shutdown follows C, B, A. C is disabled for the monitoring chain after three
consecutive failures; A and B keep running across recovery retriggers.
from datetime import datetime, timedelta, timezone
from airflow import DAG
from airflow_supervisor import Supervisor, SupervisorAirflowConfiguration, create_supervisor_control_dags
with DAG(
dag_id="ordered-services",
schedule=None,
start_date=datetime(2026, 1, 1, tzinfo=timezone.utc),
) as dag:
service = Supervisor(
dag=dag,
cfg=SupervisorAirflowConfiguration(
working_dir="/tmp/ordered-services",
runtime=timedelta(hours=8),
reference_date="start_date",
program={
"A": {"command": "/opt/services/a"},
"B": {"command": "/opt/services/b"},
"C": {"command": "/opt/services/c"},
},
program_lifecycle={
"A": {},
"B": {"start_after": ["A"], "start_delay": timedelta(minutes=10)},
"C": {
"start_after": ["B"],
"required": False,
"max_consecutive_failures": 3,
"retries": 2,
"retry_delay": timedelta(seconds=30),
},
},
),
)
globals().update(create_supervisor_control_dags(service))
The delay uses a rescheduling sensor, releasing its worker slot between checks.
An already-running B proceeds without repeating the delay on every retrigger.
Configure restart_on_initial or restart_on_retrigger only when those restarts
are intended; the defaults leave healthy programs running.
Airflow retries a failed start command twice in this example. If the final attempt still fails, C consumes one failure from its program budget. Monitoring also counts an observed process failure once until another start attempt. A healthy observation resets the consecutive failure count. A fresh scheduled or manual run starts a new budget; HA retriggers inherit it. An optional program’s failure does not select the DAG failure branch. Required program failures retain the failed retrigger path.
To use SSH, replace Supervisor and SupervisorAirflowConfiguration with
SupervisorSSH and SupervisorSSHAirflowConfiguration, adding the SSH settings from
the SSH guide. Program operations still use
XML-RPC, so workers need access to that endpoint.
For airflow-config, put the same policy under the task’s cfg:
dags:
ordered-services:
tasks:
run:
_target_: airflow_supervisor.SupervisorTask
cfg:
_target_: airflow_supervisor.SupervisorAirflowConfiguration
working_dir: /tmp/ordered-services
runtime: 28800
reference_date: start_date
program:
A: {command: /opt/services/a}
B: {command: /opt/services/b}
C: {command: /opt/services/c}
program_lifecycle:
A: {}
B: {start_after: [A], start_delay: 600}
C:
start_after: [B]
required: false
max_consecutive_failures: 3
retries: 2
retry_delay: 30
Use SupervisorSSHTask and SupervisorSSHAirflowConfiguration for SSH. After
generating the configured DAGs in memory, register controls from the native DAG:
from airflow_config import load_config
from airflow_supervisor import create_supervisor_control_dags
load_config("config", "ordered_services").generate_in_mem()
globals().update(create_supervisor_control_dags(globals()["ordered-services"]))
How to control a program independently¶
The registration above exports unscheduled ordered-services-start-C,
ordered-services-stop-C, and ordered-services-restart-C DAGs, with equivalent
controls for A and B. Aggregate start-programs, stop-programs, restart-programs,
and force-kill DAGs are also exported. Aggregate restart stops programs in shutdown
order, then starts them in startup order with the configured delays.
A stop control suspends that program through an Airflow Variable, so monitoring does not immediately recover it. Start and restart controls re-enable it and reset its failure budget. The stop intent persists across fresh runs until explicitly re-enabled. Trigger one control operation at a time for a lifecycle; separate control DAGs do not provide a transaction or distributed lock around remote actions. The daemon must already exist for program controls. Force kill terminates the daemon and remains an emergency operation, not an orderly shutdown.
To compose a control with other Airflow tasks, create it in another DAG:
with DAG(dag_id="maintenance", schedule=None) as maintenance:
stop_b = service.program_operator("B", "stop", dag=maintenance)
restart_a = service.program_operator("A", "restart", dag=maintenance, retries=2)
stop_b >> restart_a
Program stdout and stderr are forwarded by default during operations and
monitoring. Inspect the relevant task log; set forward_logs=False to opt out.