Skip to content

Pixi Sensor

Use the PixiSensor to wait for a condition that Python code checks inside a Pixi environment, for example with a client library that is not installed on the worker. It works like Airflow's PythonSensor: the callable runs on each poke, and the sensor succeeds once it returns a truthy value.

Using the sensor

from airflow.providers.pixi.sensors.pixi import PixiSensor


def landed(path: str) -> bool:
    import s3fs

    return s3fs.S3FileSystem().exists(path)


PixiSensor(
    task_id="wait_for_input",
    pypi_dependencies=["s3fs"],
    python_callable=landed,
    op_args=["s3://bucket/{{ ds }}/input.parquet"],
    poke_interval=60,
    timeout=6 * 60 * 60,
)

It accepts every Pixi Operator argument for the callable and the environment, and every BaseSensorOperator argument (poke_interval, timeout, mode, soft_fail, exponential_backoff, ...). As for the operator, parameters named after context keys such as ds or params get their values (Airflow context), and env_from_variables, env_from_connections and pixi_conn_id are read again on each poke.

Returning an XCom

PokeReturnValue is an Airflow class, which the environment doesn't have. Return its fields as a dict instead, and the sensor's XCom is xcom_value:

def newest_file(prefix: str) -> dict:
    import s3fs

    files = s3fs.S3FileSystem().ls(prefix)
    return {"is_done": bool(files), "xcom_value": max(files) if files else None}

A dict with only the keys is_done and xcom_value is read that way; any other return value is checked for truthiness.

Errors

An exception raised by the callable fails the sensor with PixiCallableError, as for the operator. As for any sensor, soft_fail does not apply to exceptions from a poke: silent_fail=True logs them and pokes again, and never_fail=True skips the sensor. To skip it from inside the callable, exit with a code listed in skip_on_exit_code.

Poke and reschedule mode

In mode="poke", the default, the environment is prepared once and every poke of the run uses it. In mode="reschedule" each poke is a new task try, so an inline environment would be built again for each one: pass env_cache_path to keep it between pokes. A relative env_cache_path or project path is relative to the DAG file.

@task.pixi_sensor

from airflow.sdk import task


@task.pixi_sensor(pypi_dependencies=["s3fs"], poke_interval=60, mode="reschedule", env_cache_path="/var/cache/pixi-airflow")
def landed(path: str) -> bool:
    import s3fs

    return s3fs.S3FileSystem().exists(path)

As for @task.sensor, a function used several times in a DAG gets unique task ids. As for @task.pixi, the function must be self-contained; see Writing the function.