## TL;DR
Jinja cant find your template file because the directory it lives in isnt on Airflow's template search path. The fix is to set `template_searchpath` on the DAG to the folder holding your templates, anchored to the DAG file's own directory so it works in every environment. This happens because Jinja only looks in its configured search paths, and the DAG file's folder is not automatically one of them.

```text
jinja2.exceptions.TemplateNotFound: my_query.sql
```

## Use this when
- A task fails at render time with TemplateNotFound naming a file you know exists
- The template works on your laptop but not on the scheduler or workers
- You recently moved templates into a subfolder like templates/

## Not for this skill when
- Jinja finds the file but chokes on its syntax (thats a template bug, not a path bug)
- The DAG file itself is missing or failing to import
- The error is a connection or permission problem, not a missing template

## Steps

1. Reproduce the failure with the exact task and logical date:

```shell
airflow tasks test my_dag my_task 2026-10-01
```
Expected output: the task fails during rendering with `jinja2.exceptions.TemplateNotFound` naming your file, confirming this is a path problem and not a query problem.

2. Inspect which search paths the task is actually using:

```python
from airflow.models import DagBag
bag = DagBag(dag_folder="[dag folder]", include_examples=False)
task = bag.get_dag("my_dag").get_task("my_task")
print(task.template_searchpath)
```
Expected output: a list of directories Jinja searches. Your templates folder is probably not in it, which is the whole bug.

3. Anchor the search path to the DAG file's directory:

```python
from pathlib import Path

TEMPLATE_DIR = str(Path(__file__).resolve().parent / "templates")

with DAG(
    dag_id="my_dag",
    template_searchpath=[TEMPLATE_DIR],
) as dag:
    ...
```
Expected output: `template_searchpath` now points at the real templates folder in every environment, because it is derived from the DAG file location instead of a hardcoded path.

4. Reference templates by filename (or subfolder-relative path) in your operators:

```python
MyOperator(
    task_id="my_task",
    sql="my_query.sql",
)
```
Expected output: the operator finds `templates/my_query.sql` at render time. If the file sits in a subfolder, use a relative path like `staging/my_query.sql`.

5. Re-run the task test from step 1.

```shell
airflow tasks test my_dag my_task 2026-10-01
```
Expected output: the task renders the template and proceeds to execute, with no TemplateNotFound in the logs.

6. If you deploy with containers or a remote executor, verify the templates exist where the worker runs, not just on your laptop:

```shell
ls [templates dir]/my_query.sql
```
Expected output: the listing shows your template file. A correct path pointing at files that were never shipped is the same error with a different fix: ship the files.

## Variant phrasings

### jinja2.exceptions.TemplateNotFound
The full exception path you see in task logs. Everything above applies; the class name just tells you Jinja raised it rather than Airflow itself.

### TemplateNotFound for a template in a subfolder
Reference it relative to the search path root, like `staging/my_query.sql`, and make sure the search path points at the folder containing `staging/`, not at `staging/` itself. Pointing one level too deep is a common variant of this bug.

### template not found only in production
Classic environment skew: the search path was built from a local absolute path, or the templates folder is not mounted in the deployed environment. Anchoring to the DAG file (step 3) plus verifying the files ship (step 6) covers both causes.

## Why it happens
Jinja's FileSystemLoader resolves template names against a fixed list of search directories, and Airflow builds that list from the DAG's `template_searchpath`. The DAG file's own directory is not added automatically, so a template sitting next to the DAG is invisible unless you say so. Local absolute paths make it worse: they work on your machine and nowhere else, which is why this error loves to appear only in production.

## Edge cases
- Case sensitivity: `My_Query.sql` and `my_query.sql` are different files on Linux. Match the filename exactly, including case.
- Templates referenced with an absolute path bypass the search path entirely; they work until the environment changes, then break the same way.
- Multiple DAGs sharing one templates folder: have each DAG compute the folder from `__file__` so moving the DAG folder doesnt silently break the path.
- Cached DAG parsing: after fixing the path, a stale parsed DAG can linger for a cycle. Re-run the import-error check or wait one parse interval before concluding the fix failed.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_qzKMoB4L_aWjEohPimEpHA
