Custom Columns
Custom columns let you implement your own generation logic using Python functions. Use them for multi-step LLM workflows, external API integration, or any scenario requiring full programmatic control. For reusable, distributable components, see Plugins instead.
Quick Start
Function Signatures
Three signatures are supported. Parameter names are validated:
For full_column strategy, use df instead of row.
For LLM access without params, use generator_params: None:
Model aliases are validated before generation starts. If an alias doesn’t exist in your config, an error is raised during the health check.
Preserving JSON values in artifacts
Set storage_encoding="json" on CustomColumnConfig when the generated column contains flexible
JSON, such as dynamic dictionaries or mixed arrays:
The generator still returns Python objects. DD encodes a storage copy and restores logical values for result loading, processors, and DD seed readers. External Parquet readers receive JSON strings; JSONL exports contain nested JSON. Encoding is fixed for the run and applies to every non-null cell, including scalar values. Only JSON-compatible values are accepted; non-string dictionary keys, non-finite numbers, and arbitrary Python objects raise errors rather than being silently converted.
Columns without this option retain their existing native Parquet behavior. Unsupported native writes raise an error identifying the affected column and the opt-in storage options.
Generation Strategies
Recommendation: Use cell_by_cell for LLM calls. The framework handles parallelization automatically. Use full_column only for vectorized operations that don’t involve LLM calls.
For full_column, set generation_strategy=dd.GenerationStrategy.FULL_COLUMN.
Synchronous cell_by_cell generator functions are dispatched concurrently across rows. Module-level mutable state (counters, caches, non-thread-safe HTTP clients) needs synchronization or per-row instantiation. For network-bound work, prefer async def fn(row) — the engine runs it directly on its event loop and skips the thread bridge.
The Decorator
Models Dict
The third argument is a dict of ModelFacade instances, keyed by alias. You must declare all models required in your custom column generator in model_aliases - this populates the models dict and enables health checks before generation starts.
This gives you direct access to all ModelFacade capabilities: custom parsers, correction loops, structured output, tool use, etc.
Other keyword arguments to generate() are sent with each model request: request fields such as temperature, max_tokens, or tool_choice, and an extra_body merged as described in Extra Body Precedence. tool_choice uses OpenAI’s format and is translated for Anthropic providers; with tool_alias, it applies only until the model’s first tool call. Arguments that aren’t request fields are dropped with a warning.
Configuration
Resizing (1:N and N:1)
Custom column generators must preserve row count. A CELL_BY_CELL generator returns one dict per input row, and a FULL_COLUMN generator returns a DataFrame with the same number of rows it received.
For expansion, filtering, aggregation, or deduplication, put the row-count-changing work at a workflow boundary. Use Workflow Chaining to run one stage, transform that stage’s output, and seed the next stage from the transformed rows.
Use cases:
- Expansion (1:N): Generate multiple variations per input between workflow stages
- Retraction (N:1): Filter, aggregate, or deduplicate records between workflow stages
Multi-Turn Example
Development Testing
Test generators with real LLM calls without running the full pipeline:
In unit tests that mock model clients, use MagicMock(spec=ModelFacade) so async methods are auto-detected:
Mocking only generate() will silently no-op because the bridge routes through agenerate().