Learning the OAA Runner Framework: Part 2 — Transforming Fields¶

This is Part 2 of the Learning the OAA Runner Framework series. It builds on Part 1 — Your First CSV Upload.
What you’ll learn: Use oaa-runner’s built-in transforms to clean and reshape CSV data before it reaches Veza: lowercase email addresses, combine first and last name, normalize boolean values, parse date strings, and store non-standard columns as custom properties.
Prerequisites¶
Completed or read Part 1 — Your First CSV Upload.
Setup¶
Create your working directory:
mkdir -p 02-csv-transform/data
cd 02-csv-transform
Copy the following content into a new file at data/users.csv — uppercase column names and inconsistent formatting are intentional:
USER_ID,EMAIL,FIRST_NAME,LAST_NAME,IS_ACTIVE,HIRE_DATE,DEPARTMENT
1001,Alice@Example.COM,Alice,Anderson,yes,2020-01-15,Engineering
1002,BOB@EXAMPLE.COM,Bob,Baker,1,2019-03-22,Engineering
1003,carol@example.com,Carol,Chen,true,2021-06-01,Marketing
Problems to solve:
EMAILarrives in mixed case — OAA expects lowercaseIS_ACTIVEuses different truthy values (yes,1,true) — OAA expects a booleanHIRE_DATEis a date string — OAA expects a parsed datetimeDEPARTMENTdoesn’t have a standard OAA field to map tonamemust combineFIRST_NAMEandLAST_NAME— neither column has the full name
Copy the following content into a new file at config.yaml:
version: v2
log_level: INFO
app:
name: Example App - CSV Transform
application_type: Custom App
sources:
users:
destination: LocalUser
source_type: csv_push
csv_filepath: data/users.csv
csv_provider_id: "!env"
field_mapping:
USER_ID:
destination_field: unique_id
You’ll add one transform at a time in the steps below.
Step 1: Lowercase the email address¶
Add a lowercase transform to the EMAIL mapping:
EMAIL:
destination_field: email
transforms:
- name: lowercase
lowercase converts the value to lowercase before storing. Email addresses are
case-insensitive but APIs and CSVs often include inconsistent casing.
Step 2: Combine first and last name¶
The CSV has separate FIRST_NAME and LAST_NAME columns, but OAA’s name field expects
a single display name. Use jinja_render to combine them:
FULL_NAME:
destination_field: name
transforms:
- name: jinja_render
options:
template: "{{ row.FIRST_NAME }} {{ row.LAST_NAME }}"
jinja_render renders a Jinja2 template against the raw source row. row.<COLUMN> reads
any column from the current record.
Note: The mapping key (
FULL_NAME) is a label you choose — it does not need to exist as a column in the CSV. Any string works. oaa-runner uses it only as an internal handle for the mapping entry. The actual data comes from the Jinja template, which readsrow.FIRST_NAMEandrow.LAST_NAMEdirectly from the source row.
Also map the individual name columns as custom properties:
FIRST_NAME:
destination_field: given_name
custom_type: STRING
LAST_NAME:
destination_field: family_name
custom_type: STRING
custom_type: STRING is added here even though given_name and family_name are
recognized field names, because this part of the config also introduces DEPARTMENT as
a custom property (Step 5). Being explicit with custom_type on all non-standard
mappings — including name fields — is clearer than mixing implicit and explicit
declarations. Part 1 omitted custom_type on these fields because custom properties
hadn’t been introduced yet.
Step 3: Normalize the boolean¶
IS_ACTIVE arrives as yes, 1, or true. Use validate_boolean:
IS_ACTIVE:
destination_field: is_active
transforms:
- name: validate_boolean
validate_boolean converts common truthy strings (yes, true, 1, on) to True
and common falsy strings (no, false, 0, off) to False. Without it, the string
"yes" fails OAA’s boolean type check.
Step 4: Parse the date¶
HIRE_DATE is an ISO date string. Use date_convert:
HIRE_DATE:
destination_field: hire_date
transforms:
- name: date_convert
date_convert parses ISO 8601 strings, Unix timestamps, and common date formats into a
normalized datetime that Veza can index.
Step 5: Store DEPARTMENT as a custom property¶
DEPARTMENT doesn’t map to a standard OAA user field. Use custom_type: STRING to store
it as a custom property on the user record:
DEPARTMENT:
destination_field: department
custom_type: STRING
custom_type tells oaa-runner to create a custom property instead of mapping to a
built-in OAA field. Supported types: STRING, BOOLEAN, STRING_LIST, INTEGER.
Built-in fields by destination type
Fields listed here map directly without
custom_type. Anything else needscustom_type.
Destination
Built-in fields
LocalUser
unique_id,name,is_active,created_at,last_login_at,password_last_changed_at,deactivated_at
LocalGroup
unique_id,name
LocalRole
unique_id,name
Employee(HRIS)
unique_id,name,employee_number,first_name,last_name,is_active,employment_status,username,department,job_title,company,cost_center,start_date,termination_date,managers
User(IdP)
identity,name,full_name,is_active,is_guest,department,manager_id
Group(IdP)
identity,name,full_name,is_security_groupSee the OAA reference docs for the complete field list per entity type.
Complete config¶
Your config.yaml should now look like this:
version: v2
log_level: INFO
app:
name: Example App - CSV Transform
application_type: Custom App
sources:
users:
destination: LocalUser
source_type: csv_push
csv_filepath: data/users.csv
csv_provider_id: "!env"
field_mapping:
USER_ID:
destination_field: unique_id
EMAIL:
destination_field: email
transforms:
- name: lowercase
FULL_NAME:
destination_field: name
transforms:
- name: jinja_render
options:
template: "{{ row.FIRST_NAME }} {{ row.LAST_NAME }}"
FIRST_NAME:
destination_field: given_name
custom_type: STRING
LAST_NAME:
destination_field: family_name
custom_type: STRING
IS_ACTIVE:
destination_field: is_active
transforms:
- name: validate_boolean
HIRE_DATE:
destination_field: hire_date
transforms:
- name: date_convert
DEPARTMENT:
destination_field: department
custom_type: STRING
Run it¶
uv tool run --from oaa-runner oaa app run --config-file=config.yaml --no-push
If it worked, you’ll see:
3 users loaded with no type errors
Email addresses converted to lowercase
Boolean values parsed correctly (
yes,1,trueall →true)HIRE_DATE parsed as a datetime
Final output:
status: true,report: []
If you see a boolean conversion error, check that IS_ACTIVE values in your CSV are in the list of supported truthy/falsy strings.
What’s next¶
These transforms handle individual field values. The next example goes further: using generators to automatically create group and role objects from column values, assigning users to them at load time — no separate groups file or hook code needed.