This document describes the Floe YAML configuration format and the meaning of
all supported options. See example/config.yml for a full working example.
version: "0.2"
metadata: { ... }
report:
path: "/abs/or/relative/report/dir"
storage: "s3_raw"
env:
file: "metadata/env.dev.yml"
vars:
incoming_path: "/data/incoming"
domains:
- name: "sales"
incoming_dir: "{{incoming_path}}/sales"
entities:
- name: "customer"
domain: "sales"
metadata: { ... }
incremental_mode: "file"
state:
path: "./state/customer.json"
source:
format: "csv"
path: "{{domain.incoming_dir}}/customer"
options:
header: true
separator: ","
encoding: "utf-8"
null_values: ["", "NULL", "null"]
recursive: false
glob: "*.csv"
cast_mode: "strict"
sink:
accepted:
format: "parquet"
path: "/path/to/accepted/"
rejected:
format: "csv"
path: "/path/to/rejected/"
archive:
path: "/path/to/archive/"
policy:
severity: "reject"
schema:
normalize_columns:
enabled: true
strategy: "snake_case"
schema_evolution:
mode: "strict"
on_incompatible: "fail"
columns:
- name: "customer_id"
type: "string"
nullable: false
unique: trueversion(required)- String used to validate config compatibility.
- Must be a numeric
major.minorstring such as"0.1","0.2", or"0.3". - Minimum supported version is
"0.1". schema.schema_evolutionrequiresversion >= "0.2".
metadata(optional)- Free-form project metadata. Keys supported in schema:
project,description,owner,tags.
- Free-form project metadata. Keys supported in schema:
report(optional)report.pathis the base directory where run reports are written. If omitted, defaults to"report".report.storage(optional) selects the storage client used for reports. Defaults tostorages.defaultwhen defined, otherwiselocal.- When
report.storageis cloud-based, reports are written via temp upload. Relativereport.pathvalues resolve under the storage prefix. - Reports are written under:
report.path/run_<run_id>/run.summary.jsonandreport.path/run_<run_id>/<entity.name>/run.json. - Supports
{{var}}templating (see "Templating & domains").
storages(optional)- Defines named storage clients for
source.storageandsink.*.storage. - If omitted,
localis assumed.
- Defines named storage clients for
catalogs(optional)- Defines named external catalogs for Iceberg and Delta sinks.
- Supported types:
glue,rest,unity. - Keys:
default(optional): default catalog name used bysink.accepted.iceberg.catalogorsink.accepted.delta.catalogwhen omitted.definitions(required): array of catalog definitions.name(required)type(required):glue,rest, orunityglue— AWS Glue Data Catalog (Iceberg, S3-backed):region(required)database(required)warehouse_storage(optional): storage name, must resolve to S3warehouse_prefix(optional): prefix for deterministic table locations
rest— Iceberg REST catalog (Unity Catalog Iceberg endpoint, Polaris, Nessie, Snowflake Open Catalog, etc.):uri(required): REST catalog base URIcredential(optional):token:<pat>orclient_credentials:<id>:<secret>;${ENV_VAR}placeholders are resolved from the OS environment at run time, so manifest runs can keep REST catalog secrets out of the manifest artifactwarehouse(optional): warehouse/prefix hint forwarded to the REST catalogoauth2_server_uri(optional): override OAuth2 server URL for client-credentials flowscope(optional): OAuth2 scopewarehouse_storage(optional): storage name for table location resolutionwarehouse_prefix(optional): prefix for deterministic table locations
unity— Databricks Unity Catalog (Delta Lake external table registration):host(required): Databricks workspace URL, e.g.https://my-workspace.azuredatabricks.netcatalog(required): Unity catalog nameschema(required): default schema nametoken(required): Personal Access Token; accepts a literal value or a single${ENV_VAR}reference resolved from the OS environment at run timecreate_schema_if_missing(optional, defaultfalse): create the Unity schema if absent
lineage(optional)- Enables OpenLineage event emission for this run.
url(required): base URL of the OpenLineage-compatible endpoint (e.g.http://marquez:5000)endpoint(optional, defaultapi/v1/lineage): path joined tourlfor POST requestsnamespace(required): OpenLineage namespace for job identitydataset_namespace(optional, defaults tonamespace): namespace for accepted Iceberg output datasetsapi_key(optional): Bearer token; supports{{VAR}}placeholder expansiontimeout_secs(optional, default5): HTTP request timeout in secondsproducer(optional): URI identifying this producer- See
docs/lineage.mdfor full event reference and lifecycle details.
env(optional)- Enables variable templating for string fields using
{{var}}syntax. env.file(optional) loads variables from a YAML file (path relative to the main config file).env.vars(optional) provides inline variables and overridesenv.file.
- Enables variable templating for string fields using
domains(optional)- Named domain roots that can be referenced by entities via
entity.domain. - Example entry:
{ name: "sales", incoming_dir: "{{incoming_path}}/sales" }.
- Named domain roots that can be referenced by entities via
entities(required)- Array of entity definitions (datasets). A single CLI run may process multiple entities.
Logical dataset name. Used in report and output paths.
Free-form entity metadata. Supported keys: data_product, domain, owner,
description, tags.
Reference to a domain defined in domains. When set, {{domain.incoming_dir}}
is available for templating within that entity.
- Controls incremental ingestion behavior for the entity.
- Supported values:
none(default)archivefilerow(reserved contract value, not runtime-enabled in this release)
incremental_mode: archiverequiressink.archive.incremental_mode: fileenables per-file state tracking and skip logic.- For backward compatibility,
sink.archivewithoutincremental_modeis still treated as archive mode, but new configs should setincremental_mode: archiveexplicitly.
- Entity-local state settings used by incremental ingestion.
state.path(optional)- Overrides the file or object used to store entity state.
- Supports local paths and cloud URIs for S3 (
s3://), GCS (gs://), and ADLS (abfs://orabfss://). - Relative paths always resolve as local filesystem paths, regardless of the source
storage context. Only explicit cloud URIs (
s3://,gs://,abfs://,abfss://) use remote storage. A relative override with a cloud source will produce local state and will not benefit from remote claim coordination. - If omitted and
incremental_mode: fileis used, Floe derives the path under the source root as.floe/state/<entity>/state.json. - For cloud sources, the derived default is a remote object under the source root,
for example
s3://bucket/prefix/incoming/.floe/state/customer/state.json.
incremental_mode: file writes state using schema floe.state.file-ingest.v2.
Existing floe.state.file-ingest.v1 files are still readable; Floe rewrites state
as v2 the next time it updates the entity.
The durable files map tracks file URIs that have completed successfully. The
claims map tracks files currently owned by a run, with run_id, acquisition
time, expiry time, size, and mtime. The default claim TTL is one hour.
Before processing, Floe removes expired claims, skips previously processed files,
and skips files actively claimed by another run with an incremental warning in
the report. New pending files are claimed with an optimistic conditional state
write before accepted sink output is written. On success, the run promotes its
claims into files; on entity failure, it removes its claims so the files can be
retried. Conditional write conflicts are retried a small fixed number of times
and then fail with a clear concurrency error.
Remote state requires object-store permissions to read, write with conditional preconditions, and delete the state object. Normal source listing permissions are still needed for input discovery; state itself does not require listing except where the selected cloud provider or IAM policy requires it for object access.
See Incremental File Ingestion for examples, CLI operations, state file shape, and CAS behavior.
format(required)- Supported:
csv,tsv,fixed,parquet,orc,json,xlsx,avro, andxml. - Cloud inputs (S3/ADLS/GCS) use temp download + local read.
jsonsupports NDJSON and JSON array modes.tsvuses tab-delimited parsing (same options ascsv).fixedreads fixed-width text files usingschema.columns[].width.xlsxreads Excel worksheets usingsource.options.sheet+ row offsets.xmlreads repeated records usingsource.options.row_tag.
- Supported:
path(required)- Input location. Can be a file, a directory, or a glob pattern
(example:
/data/in/*.csv). - If a directory is provided, a glob is applied to select files.
- Relative paths resolve against the config file directory.
- Supports
{{var}}templating (see "Templating & domains").
- Input location. Can be a file, a directory, or a glob pattern
(example:
storage(optional)- Name of the storage client to use for this source.
- Defaults to
storages.defaultwhen defined, otherwiselocal.
options(optional)- Format-specific options.
- Defaults if omitted:
header:trueseparator:";"(or"\t"whensource.formatistsv)encoding:"UTF8"null_values:[""](empty fields treated as null by default)recursive:falseglob: (none; default is based onsource.format)json_mode:"array"
glob(optional)- Used only when
source.pathis a directory. - Overrides the default file pattern for the source format:
csv:*.csvtsv:*.tsvfixed:*.txt,*.fwparquet:*.parquetorc:*.orcjson:*.jsonxlsx:*.xlsxavro:*.avroxml:*.xml
- If
source.pathitself contains a glob pattern, this option is ignored.
- Used only when
recursive(optional)- If
true, directory globs include subdirectories (via**/).
- If
json_mode(optional)array(default): JSON array ingestion whensource.format: json.ndjson: newline-delimited JSON ingestion.- Nested JSON is supported via
schema.columns[].sourceselectors. - JSON schema mismatch checks only consider top-level selector sources (no
.or[). - For safety, JSON configs are limited to 1024 columns per entity.
row_tag(required,source.format: xml)- Element name used as row boundary (each matching element becomes one record).
namespace(optional,source.format: xml)- Namespace URI used to match
row_tagand selector element tokens.
- Namespace URI used to match
value_tag(optional,source.format: xml)- Optional descendant element used when extracting text content from selector targets.
sheet(optional,source.format: xlsx)- Sheet name to read (defaults to first sheet).
header_row(optional,source.format: xlsx)- 1-based row index for column headers (default: 1).
data_row(optional,source.format: xlsx)- 1-based row index where data starts (default:
header_row + 1).
- 1-based row index where data starts (default:
cast_mode(optional)strict(default): invalid values produce cast errors.coerce: invalid values become null (and may still failnot_null).
write_mode(optional)overwrite(default): remove existing dataset parts, then write new ones.append: add new dataset parts without deleting existing ones.merge_scd1: upsert mode keyed byschema.primary_keyfor Delta or DuckDB accepted sinks.- updates matching keys (SCD1) and inserts new keys
- requires
sink.accepted.format: deltaorduckdb - requires non-empty
schema.primary_key - optional merge behavior can be configured in
sink.accepted.merge - source rows must be unique on
schema.primary_key(duplicates abort the entity merge)
merge_scd2: history mode keyed byschema.primary_keyfor Delta or DuckDB accepted sinks.- closes changed current rows and inserts new current versions
- requires
sink.accepted.format: deltaorduckdb - requires non-empty
schema.primary_key - optional merge behavior can be configured in
sink.accepted.merge - source rows must be unique on
schema.primary_key(duplicates abort the entity merge)
- Applies to both accepted and rejected outputs.
accepted(required)format:parquet,delta,iceberg, orduckdb.delta: Delta Lake transactional sink; optional Unity Catalog registration viasink.accepted.delta.iceberg: local/S3/GCS filesystem-catalog sink, with optional AWS Glue or REST catalog registration; append/overwrite supported; no schema evolution.duckdb: local.duckdbfile or MotherDuck (md:) database;overwrite/append/merge_scd1/merge_scd2via nativeMERGE INTO. Configured viasink.accepted.duckdb. See DuckDB sink.
path: output directory for accepted records.- Supports
{{var}}templating (see "Templating & domains"). storage(optional)- Name of the storage client to use for this sink target.
- Defaults to
storages.defaultwhen defined, otherwiselocal.
options(optional)- Sink tuning options (format support varies).
compression:snappy,gzip,zstd,uncompressedrow_group_size: positive integer (rows per row group)max_size_per_file: positive integer bytes (default: 256MB)- Parquet: split accepted parquet into parts at write time.
- Delta: mapped to the Delta writer target file size.
- Also bounds in-process accepted-row memory: Floe flushes the
accepted buffer to the sink whenever the accumulated estimated
in-memory size meets this threshold (or at end of entity). The
first flush uses the configured
write_mode; subsequent flushes within the same run are forced toappend. A file's archive call (whensink.archiveis enabled) always runs before its rows are added to the buffer, so a file's accepted data only ever reaches the sink after that file's archive has succeeded. - Partial-output trade-off (applies to all non-merge sinks):
because flushes commit synchronously while Phase B is still
iterating inputs, a failure on a later file (read /
validation / rejected-write / archive) can leave the earlier
files' accepted rows already committed to the sink — and in
overwritemode the first flush may already have replaced the previous dataset. The run report and incremental state are not committed in that case, so the same inputs are re-processed on the next run. - For Parquet that committed prefix is non-transactional part files in the accepted directory; downstream consumers reading the directory directly may see the torn state.
- For Delta and Iceberg each individual flush is transactional (Delta commit / Iceberg snapshot), so no single flush exposes a half-written file — but a failure on a later file does not roll back the earlier flush's commit. Readers can still observe a committed prefix of the batch between the failure and the next run, and the re-run will process the same inputs again (cross-run unique constraints or merge modes are what protect against duplication, not the sink's per-commit atomicity).
- This is an explicit design choice — keeping memory bounded
means accepting non-atomic batches. Merge modes (
merge_scd1/merge_scd2) keep the previous accumulate-then-write behaviour and ignore this memory ceiling because they require the full per-entity dataset, so they do not exhibit the cross-file partial-batch exposure.
partition_by(optional,sink.accepted.format: delta)- Identity partition columns (array of schema column names).
floe validatechecks the columns exist inschema.columns.- Runtime wiring is implemented for Delta accepted writes.
partition_spec(optional,sink.accepted.format: iceberg)- Array of partition spec entries.
- Each entry supports:
column(required): schema column nametransform(optional, defaultidentity):identity,year,month,day,hour
floe validatechecks column existence and supported transforms.- Runtime wiring is implemented for Iceberg accepted writes (table partition spec + partitioned file layout).
merge(optional, Delta and DuckDB merge modes only)- Supported only when:
sink.accepted.format: deltaorduckdbsink.write_mode: merge_scd1ormerge_scd2
ignore_columns(optional)- List of schema business columns excluded from merge update/compare behavior.
- Columns must exist in
schema.columns. schema.primary_keycolumns are not allowed.
compare_columns(optional; most relevant formerge_scd2)- Explicit list of schema business columns used for change detection.
- Columns must exist in
schema.columns. schema.primary_keycolumns are not allowed.- If omitted in
merge_scd2, Floe compares all non-key business columns minusignore_columns.
scd2(optional,merge_scd2only)current_flag_column(default__floe_is_current)valid_from_column(default__floe_valid_from)valid_to_column(default__floe_valid_to)- Custom names must be non-empty, unique, and must not collide with business schema columns.
- Supported only when:
iceberg(optional,sink.accepted.format: iceberg)- Enables Iceberg catalog-specific options.
catalog(optional)- Catalog name from
catalogs.definitions(typeglueorrest). - If omitted,
catalogs.defaultis used when set.
- Catalog name from
namespace(optional)- Iceberg namespace identifier reported with the run.
- Defaults to
entity.domainwhen present, otherwise the catalogdatabase/ REST warehouse value.
table(optional)- Logical table name for the catalog registration.
- Defaults to
entity.name(normalized).
location(optional)- Overrides the Iceberg table root location.
- Resolved via
warehouse_storagewhen defined, otherwise via the sink storage.
delta(optional,sink.accepted.format: delta)- Enables Unity Catalog registration after the Delta write.
catalog(optional)- Catalog name from
catalogs.definitions(must be typeunity). - If omitted,
catalogs.defaultis used when set.
- Catalog name from
schema(optional)- Unity schema identifier. Defaults to
entity.domain(normalized), then the catalog-levelschema.
- Unity schema identifier. Defaults to
table(optional)- Unity table name. Defaults to
entity.name(normalized).
- Unity table name. Defaults to
duckdb(optional, required whensink.accepted.format: duckdb)table(required)- Target table name. May be schema-qualified (
"schema.table").
- Target table name. May be schema-qualified (
schema(optional)- Target schema; defaults to
main.
- Target schema; defaults to
connection(optional)- MotherDuck connection string (
md:<database>). When set, the target is MotherDuck andpath/storagemust be omitted. A non-md:connection string is rejected. - When omitted, the target is the local
.duckdbfile atsink.accepted.path(object-store paths are rejected — use MotherDuck instead).
- MotherDuck connection string (
token(optional, MotherDuck only)- Literal value or a single
${ENV_VAR}reference, resolved at connect time. Never logged.
- Literal value or a single
- See DuckDB sink for full semantics.
- Accepted output reports may include file sizing metrics (
files_written,total_bytes_written,avg_file_size_mb,small_files_count) when collected by the writer.- Parquet: populated from written parquet files.
- Delta (local): populated from committed Delta log
addactions. - Delta (S3/GCS/ADLS): best-effort via object_store commit-log read; metrics remain nullable if collection fails after a successful write.
- Iceberg: populated from written Iceberg data files (metadata/manifests excluded from size totals).
table_version/snapshot_idin reports are sink-format specific (for example Delta table version, Iceberg metadata version + snapshot ID).- Compaction/optimization/maintenance remains external to Floe (for Parquet/Delta/Iceberg datasets).
- Examples: Delta optimize/vacuum, Iceberg compaction/maintenance jobs.
schema.schema_evolutionis strict by default.- Runtime support in this phase is Delta-only and additive-only.
mode: add_columnsis implemented for Deltaappend,overwrite,merge_scd1, andmerge_scd2.- Unsupported changes such as drops, renames, type changes, and non-nullable-to-nullable drift still fail the write.
- When enabled, Floe audits the outcome in the entity report
schema_evolutionblock and emits aschema_evolution_appliedlifecycle event when columns are added.
- Supports
rejected(required whenpolicy.severity: reject)format:csv(v0.1).
path: output directory for rejected rows.- Supports
{{var}}templating (see "Templating & domains"). - Rejected outputs are written as dataset parts (
part-*.csv).
- Supports
archive(optional)path: directory where raw input files are archived after ingestion.- If omitted, archiving is disabled.
- Supports
{{var}}templating (see "Templating & domains").
Column-level PII masking applied to accepted output before sink write.
pii:
columns:
- name: email
strategy: hash
- name: credit_card
strategy: mask
mask_pattern: "{first4}****{last4}"
- name: ssn
strategy: redact
redact_value: "***"
- name: notes
strategy: dropcolumns(required): array of PII column transformsname(required): schema column name (must matchschema.columns[].name)strategy(required):hash,drop,nullify,redact, ormaskmask_pattern(required forstrategy: mask): literal output string with optional{firstN}/{lastN}tokensredact_value(optional,strategy: redact): replacement string (default[REDACTED])
See docs/pii.md for full strategy reference and masking pattern syntax.
severity(required)warn: keep rows, log violations.reject: reject rows with violations (first unique kept, duplicates rejected).abort: abort the entire file on first violation.
normalize_columns(optional)enabled: boolean toggle.strategy:snake_case,lower,camel_case,none.- When enabled, both schema column names and input column names are normalized before checks. If normalization causes a name collision, the run fails.
schema_evolution(optional, v0.2)- Omitted by default, Floe behaves as
mode: strictandon_incompatible: fail. mode:strictoradd_columns.on_incompatible:fail.mode: add_columnsrequiressink.accepted.format: delta.- Current implementation is additive-only:
- new columns may be appended to existing Delta schemas
- existing columns must remain compatible
- merge-key columns cannot be introduced by evolution during Delta merge modes
- adding columns to already partitioned Delta tables is rejected in this phase
- Omitted by default, Floe behaves as
primary_key(optional)- Array of schema column names.
- Primary key columns are always treated as required (
not_null) at runtime. - Config validation rejects
nullable: trueon primary key columns. primary_keyis also enforced as a composite unique constraint.- In
sink.write_mode: merge_scd1ormerge_scd2,primary_keyis the mandatory merge key.
unique_keys(optional)- Array of unique constraints, each constraint being an array of schema column names.
- Supports single-column and multi-column uniqueness:
unique_keys: [["email"], ["id","country_code"]]
- If
unique_keysis present, legacycolumns[].uniqueflags are ignored. - Null semantics: a row is ignored for a constraint if any key component is null.
columns(required)- Array of column definitions.
name(required): target column name in the output schema.source(optional): source field selector (including nested JSON/XML extraction).- When omitted, defaults to the value of
name. - For CSV/Parquet/ORC/XLSX/Avro/fixed, use an input column name.
- For JSON, selectors may include dot notation and
[index](example:user.names[0]). - For XML, selectors support element paths with
.or/, with optional terminal attribute selection (example:order.customer.@idororder/customer/@id). - When
sourceis set,normalize_columnsapplies to the source selector for matching, but the output column name remains the explicitname. - When
sourceis set, validation summaries and row error logs include the source value alongside the column name.
- When omitted, defaults to the value of
type(required): logical type. Accepted values are case-insensitive and normalized by removing-and_.nullable(optional): defaulttrue.unique(optional, legacy): defaultfalse.- Legacy single-column unique. Prefer
schema.unique_keys.
- Legacy single-column unique. Prefer
width(required forsource.format: fixed): number of characters to read.trim(optional,source.format: fixed): trim whitespace for the column (defaulttrue).
The parser accepts the following type names (case-insensitive, - and _
ignored):
string:string,str,textboolean:boolean,boolinteger:int8,int16,int32,int64,int,integer,longunsigned integer:uint8,uint16,uint32,uint64number(float64):number,float64,float,double,decimalfloat32:float32date:datedatetime:datetime,timestamptime:time
For more details about checks and severity behavior, see docs/checks.md.
Execution order and pipeline phases are documented in docs/how-it-works.md.
Floe supports simple placeholder substitution in string fields using {{var}}
syntax. Variables are resolved in this order (lowest to highest precedence):
env.filevariables (YAML map)env.varsinline variables (override file values)
If an entity sets domain: "<name>", the following is also available:
{{domain.incoming_dir}}resolved from the matching domain entry.
Unresolved placeholders (or unknown domains) are configuration errors.