CtrlK
BlogDocsLog inGet started
Tessl Logo

wagneripjr/airflow-dags

Author, debug and configure Apache Airflow 3 DAGs: TaskFlow API, scheduling and assets, XCom, sensors, dynamic task mapping, and multi-layer test suites

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

operators.mdreferences/

Operators, Sensors, Hooks & Providers

Contents

  • Operators
  • TaskFlow Decorators
  • Sensors
  • Deferrable Operators
  • Waiting Strategies Comparison
  • Hooks
  • Provider Packages
  • Custom Components

Operators

Operators are Python classes that encapsulate a unit of work. Instantiating an operator with parameters creates a task.

Common Operators

OperatorPurposeProvider
PythonOperatorExecute Python callableapache-airflow-providers-standard
BashOperatorExecute bash commandapache-airflow-providers-standard
KubernetesPodOperatorRun in K8s podapache-airflow-providers-cncf-kubernetes
DockerOperatorRun in Docker containerapache-airflow-providers-docker
SQLExecuteQueryOperatorRun SQL queryapache-airflow-providers-common-sql
S3CreateObjectOperatorCreate S3 objectapache-airflow-providers-amazon
GCSToGCSOperatorCopy GCS objectsapache-airflow-providers-google

Operator Anatomy

from airflow.providers.standard.operators.python import PythonOperator

extract_task = PythonOperator(
    task_id="extract",           # Unique within DAG
    python_callable=my_function,  # What to execute
    op_kwargs={"param": "value"}, # Arguments to callable
    retries=3,
    retry_delay=timedelta(minutes=2),
    execution_timeout=timedelta(minutes=30),
    pool="database_pool",        # Limit concurrency
)

TaskFlow Decorators

Decorators wrap Python functions into operators with cleaner syntax:

DecoratorEquivalent OperatorUse Case
@task()PythonOperatorPython logic
@task.bash()BashOperatorShell commands
@task.virtualenv()PythonVirtualenvOperatorIsolated Python env
@task.kubernetes()KubernetesPodOperatorK8s pod execution
@task.sensor()Custom sensorWait for condition
@task.branch()BranchPythonOperatorConditional branching
@task.bash()
def run_dbt() -> str:
    return "dbt run --models staging"

@task.virtualenv(requirements=["pandas==2.0"])
def process_in_venv(data: dict) -> dict:
    import pandas as pd  # Available in virtualenv
    return data

@task.kubernetes(image="python:3.11", namespace="airflow")
def heavy_compute():
    pass

Sensors

Sensors wait for a specific condition before allowing downstream tasks to execute.

Modes

ModeBehaviorWhen to Use
pokeHolds worker slot, sleeps between checksShort waits (< 5 min)
rescheduleReleases worker slot, reschedules laterLong waits (> 5 min)

Always prefer reschedule for long waits to avoid blocking worker slots.

Common Sensors

SensorWaits For
S3KeySensorFile exists in S3
HttpSensorHTTP endpoint returns success
SqlSensorSQL query returns truthy result
ExternalTaskSensorTask in another DAG completes
FileSensorFile exists on filesystem
DateTimeSensorSpecific datetime reached

Sensor Parameters

from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor

wait_for_file = S3KeySensor(
    task_id="wait_for_file",
    bucket_name="my-bucket",
    bucket_key="incoming/{{ ds }}/data.csv",
    aws_conn_id="aws_default",
    mode="reschedule",           # Release worker slot
    poke_interval=300,           # Check every 5 minutes
    timeout=3600,                # Fail after 1 hour
    exponential_backoff=True,    # Increase interval between checks
    max_wait=timedelta(minutes=30),  # Max interval with backoff
)

Custom Sensor with @task.sensor

from airflow.sensors.base import PokeReturnValue

@task.sensor(mode="reschedule", poke_interval=60, timeout=3600)
def wait_for_api(**context) -> PokeReturnValue:
    from airflow.providers.http.hooks.http import HttpHook
    hook = HttpHook(http_conn_id="api_default")
    response = hook.run(endpoint="/status")
    is_ready = response.json().get("ready", False)
    return PokeReturnValue(is_done=is_ready, xcom_value=response.json())

Deferrable Operators

Use Python asyncio to efficiently wait for external resources without occupying a worker slot. Requires the triggerer service running.

When to Use

  • Any task waiting > 1 minute on external systems
  • Sensors with long polling intervals
  • Tasks that call slow APIs or wait for batch jobs

How It Works

  1. Task starts on worker, initiates external operation
  2. Task defers — releases worker slot, creates a Trigger
  3. Trigger runs on triggerer service using asyncio (lightweight)
  4. When condition is met, trigger fires
  5. Task resumes on a worker to complete

Enabling

# Enable deferrable mode globally for all operators that support it
AIRFLOW__OPERATORS__DEFAULT_DEFERRABLE=True

Or per-operator: use the deferrable version (many providers offer both):

# Standard (blocks worker)
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor

# Deferrable (releases worker) — often same class with deferrable=True
S3KeySensor(
    task_id="wait",
    deferrable=True,  # Uses triggerer instead of worker
    ...
)

Cost Impact

Deferrable operators significantly reduce worker costs. A sensor waiting 2 hours:

  • Poke mode: Blocks 1 worker slot for 2 hours
  • Reschedule mode: Intermittently blocks slot, occupies scheduler
  • Deferrable: Zero worker slots, minimal triggerer resources

Waiting Strategies Comparison

Choose the right mechanism for waiting on external events:

MechanismHow It WorksWorker ImpactBest For
Sensor (poke)Holds worker slot, sleeps between checksHigh — blocks 1 slotShort waits (< 5 min), simple conditions
Sensor (reschedule)Releases slot, scheduler reschedulesMedium — intermittentMedium waits (5-60 min)
Deferrable operatorAsync trigger on triggerer serviceZero — no worker slotLong waits (> 1 min), slow APIs, batch jobs
AssetWatcherAlways-on background polling via triggererZero — no task createdExternal event → DAG trigger (Kafka, SQS)

Decision Guide

  • "Wait for a file, then process it" → Deferrable S3KeySensor (deferrable=True)
  • "Wait for a quick API response" → Sensor with mode="poke" or @task.sensor()
  • "Run DAG when a Kafka message arrives" → AssetWatcher + MessageQueueTrigger
  • "Wait for another DAG's task to complete"ExternalTaskSensor (reschedule mode) or Assets
  • "Process data whenever upstream produces it" → Asset-based scheduling (no sensor needed)

Rule of thumb: If the wait is > 1 minute, use deferrable. If you want to trigger a DAG (not a task), use AssetWatcher. Sensors are for in-DAG waiting only.

Hooks

Hooks abstract external system connections. Used inside operators, decorators, or task code.

Common Hooks

HookExternal System
S3HookAWS S3
PostgresHookPostgreSQL
HttpHookREST APIs
MySqlHookMySQL
GCSHookGoogle Cloud Storage
SlackHookSlack

Usage

@task()
def fetch_from_postgres(**context) -> list[dict]:
    from airflow.providers.postgres.hooks.postgres import PostgresHook
    hook = PostgresHook(postgres_conn_id="my_postgres")
    df = hook.get_pandas_df("SELECT * FROM users WHERE date = %(ds)s", parameters={"ds": context["ds"]})
    return df.to_dict(orient="records")

Always use hooks instead of raw clients (boto3, requests, psycopg2). Hooks use Airflow connections for credential management.

Provider Packages

Providers are Python packages containing operators, sensors, hooks, and connections for specific systems. 300+ available.

Installation

pip install apache-airflow-providers-amazon
pip install apache-airflow-providers-google
pip install apache-airflow-providers-postgres

Or in requirements.txt for Astro CLI projects.

Finding Providers

Version Compatibility

Provider versions must be compatible with your Airflow version. Check the provider's PyPI page or Astronomer Registry for compatibility matrix.

Custom Components

Custom Operator

from airflow.models import BaseOperator

class MyCustomOperator(BaseOperator):
    template_fields = ("source", "destination")  # Jinja-templatable fields

    def __init__(self, source: str, destination: str, **kwargs):
        super().__init__(**kwargs)
        self.source = source
        self.destination = destination

    def execute(self, context):
        # Task logic here
        self.log.info(f"Processing {self.source} -> {self.destination}")
        return {"status": "success"}

Custom Hook

from airflow.hooks.base import BaseHook

class MyApiHook(BaseHook):
    conn_name_attr = "my_api_conn_id"
    default_conn_name = "my_api_default"
    conn_type = "http"

    def __init__(self, my_api_conn_id: str = default_conn_name):
        super().__init__()
        self.my_api_conn_id = my_api_conn_id

    def get_conn(self):
        conn = self.get_connection(self.my_api_conn_id)
        return {"base_url": conn.host, "token": conn.password}

    def fetch_data(self, endpoint: str) -> dict:
        config = self.get_conn()
        # Implementation here
        pass

Custom Sensor

from airflow.sensors.base import BaseSensorOperator

class MyCustomSensor(BaseSensorOperator):
    def __init__(self, target_value: str, **kwargs):
        super().__init__(**kwargs)
        self.target_value = target_value

    def poke(self, context) -> bool:
        # Return True when condition is met
        current = check_external_system()
        return current == self.target_value

Packaging Custom Components

For reuse across DAGs and teams, package as a Python package:

my_airflow_components/
  __init__.py
  operators/
    __init__.py
    my_operator.py
  hooks/
    __init__.py
    my_hook.py
  sensors/
    __init__.py
    my_sensor.py
  setup.py

Install via pip install -e . or publish to private PyPI.

SKILL.md

tile.json