Cookbook: an Airflow report after the worker exits
Upload a closed report from an Airflow task, pass only an id through XCom, and check the same bytes from a fresh process.
An Airflow task can build a report and then the worker exits. The next run does not have that local file.
mountable.io is a persistent filesystem for AI agents. This recipe uploads the closed report before the task returns an id. A later process reads that same file.
mountable.io maintains the runnable example, separately from Apache Airflow. The report is synthetic. It holds no customer data. The id is the XCom value. The bytes stay in the file. This is not an Airflow provider, and it does not store Airflow metadata.
Prerequisites
Use an existing mountable.io account with a saved payment method, an existing filesystem and an API key that can access it. Storage and file operations follow the usage prices. The example creates no account, filesystem or credits.
You also need Python 3.12 and uv. Set MOUNTABLE_API_KEY in your private
environment or secret manager. Never put a key in a command argument.
git clone https://github.com/writeitai/mountable.git
cd mountable/examples/airflow-task-report
uv sync --locked --python 3.12
export MOUNTABLE_FILESYSTEM_ID="your-filesystem-id"
export REPORT_STORE=mountableThe example pins Apache Airflow 3.3.2 and the Python SDK. It
runs airflow dags test against a disposable local database. It does not
mount the filesystem, and it does not need an Airflow deployment. For one
filesystem and a hard monthly spend cap per end user, follow the
capped workspace guide. This recipe uses
one existing filesystem and a path your backend chooses.
The report belongs to the fixture user alice. Your own DAG would choose
its real user. It would still choose the filesystem and the path, and it
would not take either from the task arguments.
Write the report
uv run python report.py writeA successful run prints two lines:
report_id=a1b2c3d4e5f60789
match reports/alice/a1b2c3d4e5f60789.txt sha256=<hash> bytes=85The id is 16 lowercase hex characters. It is not a filesystem id and it is
not a path. The command runs one DAG, synthetic_report, with
airflow dags test. The producer task writes reports/alice/<id>.txt only
after it has closed the report, then returns that id. The reader task
receives the id and checks the file. The write uses If-None-Match: *, so
an existing file is left as it is.
The file looks like this, with that run's id:
report_id: a1b2c3d4e5f60789
user: alice
kind: synthetic
item,quantity
alpha,1
beta,2If the upload fails, the producer task fails. The command exits without a
report_id= line.
The tasks pass the id only. They do not receive a filesystem id or a path. The report bytes are not the XCom value.
Read it from a fresh process
Start a new process with the same environment. This process does not run an Airflow DAG. Then read the report:
uv run python report.py read --report-id "$ID"The command opens a read-only handle. A matching run prints one line:
match, the path, the SHA-256 and the size of the stored file. It prints
that line only when those bytes are the report the writer uploaded.
A second create of the same id does not replace the file:
uv run python report.py overwrite --report-id "$ID"That command runs airflow dags test replace_report and fails. The first
report stays as it is. A read-only handle cannot write it either:
uv run python report.py read-only-write --report-id "$ID"One command runs that check for you. It runs the DAG, reads the report from a new process, rejects a replacement, rejects a read-only write, and deletes the file:
uv run python report.py liveIt prints match, the path, the SHA-256 and the size. It does not
print the API key.
A 402 with filesystem_spending_cap_reached stops that filesystem's
billable mountable.io work. Other filesystems keep working.
The id the task returns is the handoff. The bytes stay in the file. The
local Airflow database stays in .airflow on the machine that ran the test.
Delete that directory when you no longer need those local DAG runs.
Clean up
uv run python report.py cleanup --report-id "$ID"This deletes that one file. A file that is already gone is left gone.
Offline checks
From the example directory:
uv run pytest -qThe tests call the store with a fake, and they also run airflow dags test
over a temporary directory. They make no paid cloud calls. They check that
XCom holds the id and not the report body, that a failed upload fails the
DAG, and that a second create leaves the first bytes in place.
Sources: Pythonic DAGs with the TaskFlow API
and airflow dags test,
checked against Apache Airflow 3.3.2 on 2026-10-27, and the
Python SDK.