BashOperator

Use the BashOperator to execute commands in a Bash shell.

airflow/example_dags/example_bash_operator.py[source]

  1. run_this = BashOperator(
  2. task_id="run_after_loop",
  3. bash_command="echo 1",
  4. )

Templating

You can use Jinja templates to parameterize the bash_command argument.

airflow/example_dags/example_bash_operator.py[source]

  1. also_run_this = BashOperator(
  2. task_id="also_run_this",
  3. bash_command='echo "ti_key={{ task_instance_key_str }}"',
  4. )

Warning

Care should be taken with “user” input or when using Jinja templates in the bash_command, as this bash operator does not perform any escaping or sanitization of the command.

This applies mostly to using “dag_run” conf, as that can be submitted via users in the Web UI. Most of the default template variables are not at risk.

For example, do not do this:

  1. bash_task = BashOperator(
  2. task_id="bash_task",
  3. bash_command='echo "Here is the message: \'{{ dag_run.conf["message"] if dag_run else "" }}\'"',
  4. )

Instead, you should pass this via the env kwarg and use double-quotes inside the bash_command, as below:

  1. bash_task = BashOperator(
  2. task_id="bash_task",
  3. bash_command="echo \"here is the message: '$message'\"",
  4. env={"message": '{{ dag_run.conf["message"] if dag_run else "" }}'},
  5. )

Skipping

In general a non-zero exit code produces an AirflowException and thus a task failure. In cases where it is desirable to instead have the task end in a skipped state, you can exit with code 99 (or with another exit code if you pass skip_exit_code).

airflow/example_dags/example_bash_operator.py[source]

  1. this_will_skip = BashOperator(
  2. task_id="this_will_skip",
  3. bash_command='echo "hello world"; exit 99;',
  4. dag=dag,
  5. )

Troubleshooting

Jinja template not found

Add a space after the script name when directly calling a Bash script with the bash_command argument. This is because Airflow tries to apply a Jinja template to it, which will fail.

  1. t2 = BashOperator(
  2. task_id="bash_example",
  3. # This fails with 'Jinja template not found' error
  4. # bash_command="/home/batcher/test.sh",
  5. # This works (has a space after)
  6. bash_command="/home/batcher/test.sh ",
  7. dag=dag,
  8. )

However, if you want to use templating in your bash script, do not add the space and instead put your bash script in a location relative to the directory containing the DAG file. So if your DAG file is in /usr/local/airflow/dags/test_dag.py, you can move your test.sh file to any location under /usr/local/airflow/dags/ (Example: /usr/local/airflow/dags/scripts/test.sh) and pass the relative path to bash_command as shown below:

  1. t2 = BashOperator(
  2. task_id="bash_example",
  3. # "scripts" folder is under "/usr/local/airflow/dags"
  4. bash_command="scripts/test.sh",
  5. dag=dag,
  6. )

Creating separate folder for bash scripts may be desirable for many reasons, like separating your script’s logic and pipeline code, allowing for proper code highlighting in files composed in different languages, and general flexibility in structuring pipelines.

It is also possible to define your template_searchpath as pointing to any folder locations in the DAG constructor call.

Example:

  1. dag = DAG("example_bash_dag", template_searchpath="/opt/scripts")
  2. t2 = BashOperator(
  3. task_id="bash_example",
  4. # "test.sh" is a file under "/opt/scripts"
  5. bash_command="test.sh ",
  6. dag=dag,
  7. )

BashSensor

Use the BashSensor to use arbitrary command for sensing. The command should return 0 when it succeeds, any other value otherwise.

airflow/example_dags/example_sensors.py[source]

  1. t3 = BashSensor(task_id="Sensor_succeeds", bash_command="exit 0")
  2. t4 = BashSensor(task_id="Sensor_fails_after_3_seconds", timeout=3, soft_fail=True, bash_command="exit 1")