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:

  1. Is this a field rename or combination? → jinja_render

  2. Is this a date format problem? → date_convert

  3. Is this an email/string normalization? → lowercase or jinja_render

  4. Is this a field that doesn’t fit a standard OAA type? → custom_type

  5. Is 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.

See also