Exhaust config.yaml before writing a hook¶
When building an OAA-runner integration, reaching for a hook function feels natural the moment the data doesn’t quite fit. The source field is named differently, the date format is wrong, you need to combine two fields into one. It’s tempting to drop into Python immediately because you can do anything there.
But hooks come with a real cost. A hook is Python code that runs against every record in a source. It needs to be maintained, tested, and understood by anyone who picks up the integration later. It also breaks the declarative contract of the config — a reader can no longer understand the integration by reading the YAML alone.
The right question to ask before writing a hook is: can config.yaml already do this?
The answer is yes more often than you’d expect.
What config.yaml can do without a hook¶
Field renaming¶
When your source field name doesn’t match the OAA destination field name, use jinja_render to pull the value from the raw row data:
field_mapping:
name:
destination_field: name
transforms:
- name: jinja_render
options:
template: "{{ row.display_name }}"
This reads display_name from the source record and maps it to the OAA name field — no Python required.
Combining fields¶
jinja_render accepts a full Jinja2 template, so you can combine multiple source fields:
field_mapping:
name:
destination_field: name
transforms:
- name: jinja_render
options:
template: "{{ row.first_name }} {{ row.last_name }}"
Date normalization¶
Source APIs return dates in dozens of formats. date_convert handles ISO 8601, Unix timestamps, and common date strings automatically:
field_mapping:
created_at:
destination_field: created_at
transforms:
- name: date_convert
You can chain it with jinja_render if the date lives in a differently-named field:
field_mapping:
last_login_at:
destination_field: last_login_at
transforms:
- name: jinja_render
options:
template: "{{ row.last_login }}"
- name: date_convert
Lowercasing¶
Email addresses in particular need to be normalized. lowercase does it in one line:
field_mapping:
email:
destination_field: email
transforms:
- name: lowercase
Custom properties¶
Any source field that doesn’t map to a standard OAA field can be stored as a custom property — searchable and visible in Veza — with just a custom_type annotation:
field_mapping:
department:
destination_field: department
custom_type: STRING
is_mfa_enabled:
destination_field: mfa_enabled
custom_type: BOOLEAN
tag_list:
destination_field: tags
custom_type: STRING_LIST
as_list: true
Resource hierarchies¶
If your resources have a parent-child relationship, the tree config block builds the hierarchy without any code:
sources:
resources:
destination: Resource
resource_type: Resource
field_mapping:
unique_id:
destination_field: unique_id
name:
destination_field: name
tree:
id_field: unique_id
parent_id_field: parent_id
When a hook is actually necessary¶
Hooks exist for relationships and logic that genuinely cannot be expressed as a field mapping. The clearest signal that a hook is warranted:
Cross-record relationships. When you need to link an entity in one source record to an entity defined in another — for example, assigning a role to a user based on an assigned_user_ids list on the role record — that requires iterating over the provider object graph at record-transform time. config.yaml has no syntax for this.
@record_transform_non_stream(target='roles')
def link_role_users(record, provider=None, **kwargs):
for uid in getattr(record, 'assigned_user_ids', []):
user = provider.local_users.get(uid)
if user:
user.add_role(record.unique_id, apply_to_application=True)
Filtering based on computed conditions. If you need to skip records that fail a multi-field condition that can’t be expressed as a PETL transform, a pre-transform hook is the right tool.
External lookups. If resolving a field requires a secondary API call — for instance, fetching a user’s manager’s email using a manager_id — that’s a hook.
If none of these apply to your situation, check the transform reference before writing Python. The config usually has it covered.
The practical test¶
Before writing a hook, ask:
Is this a field rename or combination? →
jinja_renderIs this a date format problem? →
date_convertIs this an email/string normalization? →
lowercaseorjinja_renderIs this a field that doesn’t fit a standard OAA type? →
custom_typeIs this a resource hierarchy? →
tree
If the answer to all five is no, then you have a genuine hook use case. Otherwise, keep the logic in the config where it’s readable, testable without Python, and requires no deployment artifact beyond a YAML file.