What is the OAA provider object?

The provider object passed to hook functions is the in-memory graph of all OAA objects loaded during the current run. It holds every user, group, role, resource, and employee that has been created by the sources up to that point, keyed by their unique_id.

Hooks receive it via the provider keyword argument:

@record_transform_non_stream(target='roles')
def assign_roles(record, provider=None, **kwargs):
    user = provider.local_users.get(record.user_id)

Attribute reference

Attribute

Type

Used when

provider.local_users

dict[str, LocalUser]

Custom App: look up users by unique_id

provider.local_groups

dict[str, LocalGroup]

Custom App: look up groups by unique_id

provider.local_roles

dict[str, LocalRole]

Custom App: look up roles by unique_id

provider.resources

dict[str, Resource]

Resource-scoped permissions: look up resources by unique_id

provider.employees

dict[str, Employee]

HRIS destination: look up employees by unique_id

provider.users

dict[str, IdentityUser]

IdP destination: look up identity users

provider.groups

dict[str, IdentityGroup]

IdP destination: look up identity groups

All dicts use the object’s unique_id as the key. .get(id) returns None if the ID is not found — always check before using the result.

Common methods on loaded objects

LocalUser:

user.add_group('group-id')
user.add_role('role-id', apply_to_application=True)
user.add_role('role-id', resources=[resource], apply_to_application=False)

LocalGroup:

group.add_role('role-id', apply_to_application=True)

IdentityGroup (IdP):

group.add_user('identity@example.com')

LocalUser Built-in Field Mapping Reference

When creating field mappings in config.yaml, it’s critical to distinguish between built-in LocalUser fields (which do NOT use custom_type) and custom properties (which require custom_type: STRING/NUMBER/BOOLEAN).

Built-in LocalUser Fields

These fields are built-in to the OAA LocalUser model and have special handling. They should NEVER use custom_type in field mappings.

destination_field

Required?

Notes

Example

unique_id

✓ Yes

Unique identifier for the user (primary key). Must be unique across all users.

id, username, employee_id

name

—

Display name. Use for full name. Do NOT use first_name or last_name.

full_name → name

email

—

Email address. Use for the email field. Do NOT use identity or identities.

email_address → email

is_active

—

Boolean: true or false. Whether the user account is active.

active_flag → is_active

user_type

—

Enum: SERVICE_ACCOUNT, END_USER, or SHARED. Account type/classification.

account_type → user_type

last_login_at

—

ISO 8601 timestamp of last login. Use date_convert transform for non-standard formats. Do NOT use last_login.

last_login_time → last_login_at

created_at

—

ISO 8601 timestamp when user was created.

created_date → created_at

deactivated_at

—

ISO 8601 timestamp when user was deactivated/deleted.

deleted_date → deactivated_at

password_last_changed_at

—

ISO 8601 timestamp of last password change.

pwd_changed → password_last_changed_at

Fields That Are NOT Built-in

These fields do not exist as built-in LocalUser attributes. Do NOT attempt to map them directly. Instead, use custom fields or combine into name:

Field Name

Why NOT built-in

Solution

first_name

Not a built-in attribute

Use name field or create first_name as custom_type: STRING

last_name

Not a built-in attribute

Use name field or create last_name as custom_type: STRING

identity

Not a built-in attribute; confusing name

Use email field instead

identities

Not a built-in attribute

Use email field instead

manager_id

Use relationships instead

Use PRE_PUSH_OAA hook: user.add_manager(manager_id)

Correct vs. Incorrect Examples

Incorrect (using non-existent built-in fields):

field_mapping:
  first_name:
    destination_field: first_name  # ❌ Not a built-in field
  
  last_name:
    destination_field: last_name    # ❌ Not a built-in field
  
  login:
    destination_field: identity     # ❌ Not a built-in field; confusing
  
  pwd_change_date:
    destination_field: last_login   # ❌ Wrong field name (last_login_at)

Correct (using built-in fields properly):

field_mapping:
  user_id:
    destination_field: unique_id    # ✓ Built-in, required
  
  email_addr:
    destination_field: email        # ✓ Built-in
  
  full_name:
    destination_field: name         # ✓ Built-in; use for full name
  
  last_login_time:
    destination_field: last_login_at     # ✓ Built-in, correct name
    transforms:
      - name: date_convert
  
  is_disabled:
    destination_field: is_active    # ✓ Built-in boolean
    transforms:
      - name: jinja_render
        parameters:
          template: "{{not row.is_disabled}}"

Correct (using custom fields for non-built-in data):

field_mapping:
  user_id:
    destination_field: unique_id
  
  email:
    destination_field: email
  
  first_name:
    destination_field: first_name   # ✓ Custom field
    custom_type: STRING
  
  last_name:
    destination_field: last_name    # ✓ Custom field
    custom_type: STRING
  
  department_code:
    destination_field: department   # ✓ Custom field
    custom_type: STRING
  
  employee_start_date:
    destination_field: hire_date    # ✓ Custom field
    custom_type: TIMESTAMP
    transforms:
      - name: date_convert

Timestamp Fields — Special Handling

Built-in timestamp fields (last_login_at, created_at, deactivated_at, password_last_changed_at) must be either valid RFC3339 format or NULL. Empty strings are rejected by Veza.

See What are Veza Custom Properties? for cleanup patterns.

For Other Entity Types

For LocalGroup, LocalRole, Employee, and other entity types, refer to the Veza Developer Documentation for built-in field lists.

Notes

  • All attributes are populated by the time record_transforms_non_stream hooks run. oaa-runner loads all source records before running post-load hooks.

  • provider.local_groups holds Custom App groups (created by destination: LocalGroup sources). provider.groups holds IdP groups (created by destination: IdpGroup sources). They are separate dicts even in multi-destination configs.

  • Setting a field directly on a loaded object is also valid: record.some_custom_field = 'value'

See also