The gwenlake.transforms layer lets you declare what a function reads and writes with decorators. The platform registers the function from those bindings, runs it in a build and records the lineage. Datasets and models are addressed by their handle, "<project_slug>.<slug>", the same one used in SQL. Bindings are matched to the function’s parameters by keyword name.

transform_df: DataFrames in, DataFrame out

The function receives each Input as a pandas.DataFrame and returns the DataFrame to write to the single Output. The result is written for you, replacing the dataset’s content by default.
With several outputs, return a dict keyed by their names:

transform: explicit reads and writes

The lower-level form receives TransformInput and TransformOutput objects and does its own IO. Use it for append mode, for chunked processing, or for files.
mode="replace" (default) clears the dataset first and writes a snapshot; mode="append" keeps what is there and adds the file. write_dataframes(chunks) streams an iterator of DataFrames out as part files, see Large datasets.

Large datasets

A transform_df loads each input whole. When an input does not fit, use transform and stream.

Read less

columns= restricts a read to the named top-level columns, on the binding or per call. It is the single most effective way to cut the cost of a wide dataset: a transform that uses ten of two hundred columns otherwise pays to move all two hundred. chunk_size and order_by steer the paging.

Chunked reads and writes

In a transform, iter_dataframes() pages through the input with LIMIT/OFFSET and yields pandas.DataFrame chunks; write_dataframes() streams them back out as part-00000.parquet, part-00001.parquet, …
Always pass order_by= so that pages are split deterministically.

Local Parquet, batches, row count

For libraries that want a file, to_parquet() writes the whole input to a local Parquet file, in chunks, and returns the row count; with reuse=True (default) an existing complete file is kept, so a resumed run skips the download.
batches(size) returns a callable that yields batches of size rows, ready for a training loop’s run.steps(...), see Models. input.rows (also len(input)) is the row count, counted server-side with one COUNT(*) and kept for the run, to size a loop before reading anything.

Working with files

.filesystem() on an input or an output gives a small filesystem over the dataset: ls(path=, glob=), read, write, delete, clear and open. Use it for images, PDFs and any raw file.
open(..., "rb") returns the downloaded bytes; open(..., "wb") returns a buffer that uploads on close.

Other bindings

Several outputs, one build

A function with several Outputs is registered as one transform per output, and the platform knows they are one program: running any of them runs the function once and rewrites every output, a second build of the same function is refused while one is running, and the outcome is recorded on every output. The outputs are written one after the other, in declaration order, without atomicity between them, and a failure’s message says so.

Incremental transforms

A transform that must not redo what it has already done keeps its memory in one of its own outputs, a log of what it has processed, and reads it back on the next run. An output can never be an input of the same transform (that would be a cycle in the lineage), so the @incremental decorator gives outputs a "previous" reading mode and makes writes add to the dataset instead of replacing it. Stack it above @transform or @transform_df:
The source here is a connection, read whole on every run: the log is what remembers. The same pattern works with a dataset as the source.

Reading an output

"previous" is read before the run’s first write and memoized: ask for it afterwards and the library refuses rather than answer a mix of old and new rows. schema shapes the empty frame of a first run; pass a list of column names, a {name: dtype} mapping, a pyarrow.Schema or a DataFrame to copy the columns of. columns= restricts the read, which is all a log of paths needs:

Writing

Under @incremental a write adds to the output: an append into a tabular dataset, or one more part file into a file dataset, named part-<build>-<n>.parquet so two runs never overwrite each other. Several writes in one run add up. To start the output over in one run, call output.set_mode("replace") before the first write, or pass mode="replace" to it: the first write then replaces the dataset and the following ones add to it.

Starting over: semantic_version

Bump semantic_version when the logic changes in a way that invalidates what was written: the next build starts over, "previous" comes back empty and the first write replaces. The platform records, after each successful build, the version each output was written with; a run whose decorator says another version is a run that starts over. A run that starts over and finds nothing to write still empties the output, so the bump means what it says. Leave the version alone for an ordinary code change: the log is the memory, and it survives deployments.

What a build records

  • On success, the platform stamps every output of the transform with the semantic_version the code declared and, for tabular inputs, the point each one was read up to (see below). This is what the next run compares itself to.
  • On failure, nothing is recorded. What the run appended before failing stays in the output and is the "previous" content of the next run, which is why the log is written last: a failure between the two writes re-processes the files, it never forgets them. The build’s error message says so.

Reading what was added to a tabular input

Inside a build, a tabular input also answers dataframe("added"): the rows appended since the last successful run, read from the table’s own snapshot history. Before the run starts, the platform pins where each tabular input stands; "added" reads up to that point and never “now”, so a row landing while the run is going is read once, by the next run. dataframe("previous") is the table as the last run saw it.
iter_dataframes("added", chunk_size=...) streams the same rows. Both take columns= and order_by=. Three cases have no delta, and the library or the platform says so instead of quietly answering with the whole table:
  • a file dataset, a connection, or a run outside a build: nothing keeps their history, so "added" and "previous" are refused and the input is read whole with dataframe();
  • a table rewritten in between: a snapshot upload, a delete or an upsert changes rows the previous run had already seen, so the rows added since do not describe what changed. The read fails with a message that says to read the table whole or bump semantic_version;
  • a snapshot expired by the dataset’s maintenance before the next run: same answer. Keep the maintenance window longer than the interval between two runs of the transform.
"added" is a transform feature: a @transform_df body receives its inputs as whole DataFrames. Under @incremental, the DataFrame a @transform_df returns is added to its output rather than replacing it, and a parameter named after an output receives the output handle, so the body may read its "previous" or set_mode it first.

Options accepted for compatibility

@incremental also accepts snapshot_inputs, require_incremental and allow_retention, so a script written against the same decorator elsewhere runs as is. They do nothing here: every input is already read whole unless "added" is asked for, the platform never falls back to a full rebuild on its own, and nothing deletes from an input behind a transform. @incremental is not for @train: a model is replaced by its training, never appended to.

Dependencies and settings

A build runs the entrypoint with uv run from the repository root, so the repository’s pyproject.toml and uv.lock (or PEP 723 inline metadata in the script) decide what is installed. Three settings on the transform add to that without a commit: packages (extra package specs), requirements (a requirements file in the repository) and python (an interpreter version). A fourth, parameters, is a JSON object the run receives as TRANSFORM_PARAMETERS: one transform attached to several datasets reads its chunk size or its model name from there. See Builds.

Running

Call the decorated function with a client to run it from your machine. In a repository the platform builds, use run as the module’s entrypoint, so a build runs only the transforms it targets:
The transforms layer is synchronous. It uses the sync Gwenlake client even in an async program.