How to Use Generators to Create Groups and Roles from Field Values¶
Goal¶
Automatically create LocalGroup or LocalRole objects from unique field values in
your source data — without writing a separate source or custom hook.
For example, if your users CSV contains a department column, a generator can read
each unique department name and create a corresponding LocalGroup, then link each
user to the group that matches their department.
When to Use Generators¶
Use a generator when:
You want to create groups or roles derived from data on another object (e.g. create a group per department, a role per job title).
The groups/roles don’t exist as a dedicated source in your data.
You want membership to be assigned automatically at the same time.
If your groups or roles already exist as a dedicated table or file, use a separate
source with destination: LocalGroup or destination: LocalRole instead.
How Generators Work¶
Generators are declared inline under a sources.<name>.generators section. For each
record in the source, the runner:
Reads the value of
source_fieldfrom the current record.Renders
name_templateandunique_id_templateusing that value.Creates a
LocalGrouporLocalRolewith the rendered name and unique ID — or retrieves the existing one if it was already created by a previous record.Optionally calls
add_group()oradd_role()on the primary source object (e.g. theLocalUser) to assign membership.
This means deduplication is automatic: 100 users with department = "Engineering"
produce exactly one group.
Step-by-Step Example¶
1. Identify the source field¶
Find the column in your source data that contains the values you want to turn into groups or roles. In this example the source is a CSV of users:
login,email,department,job_title
alice@example.com,alice@example.com,Engineering,Backend Engineer
bob@example.com,bob@example.com,Marketing,Content Strategist
carol@example.com,carol@example.com,Engineering,Frontend Engineer
The columns department and job_title will each drive a generator.
2. Add a generators block to your source¶
version: v2
app:
name: My App
application_type: MyApp
sources:
users:
destination: LocalUser
source_type: csv
csv_filepath: data/users.csv
field_mapping:
login:
destination_field: unique_id
email:
destination_field: email
department:
destination_field: department
job_title:
destination_field: job_title
generators:
department_groups:
generator_type: local_group
source_field: department
name_template: "{{value}}"
unique_id_template: "dept_{{value}}"
add_membership: true
skip_empty: true
title_roles:
generator_type: local_role
source_field: job_title
name_template: "{{value}}"
unique_id_template: "role_{{value}}"
add_membership: true
skip_empty: true
3. Understand the config keys¶
Key |
Required |
Default |
Description |
|---|---|---|---|
|
✅ |
— |
|
|
✅ |
— |
Field name to read values from |
|
|
Jinja2 template for the object’s |
|
|
|
Jinja2 template for the object’s |
|
|
|
Link the source record to the generated object |
|
|
|
Skip records where |
|
|
|
Treat |
|
|
|
When |
|
|
— |
Optional: set extra properties on the generated object |
4. Template variables¶
Both name_template and unique_id_template are Jinja2 templates. Two variables
are available:
{{value}}— the value read fromsource_fieldfor the current record.{{row}}— the full source record (access other fields as{{row.department}}).
Examples:
# Simple: use the value directly
name_template: "{{value}}"
# Add a prefix
unique_id_template: "dept_{{value}}"
# Combine fields
name_template: "{{row.division}} / {{value}}"
5. Set additional properties using field_mapping (optional)¶
You can set extra properties on the generated object using an optional field_mapping
block inside the generator. Only convert-mode transforms are supported here (no
stream-level add or select transforms).
generators:
department_groups:
generator_type: local_group
source_field: department
name_template: "{{value}}"
unique_id_template: "dept_{{value}}"
add_membership: true
field_mapping:
department_label:
source_field: department
destination_field: description
transforms:
- name: jinja_render_convert
options:
template: "Department: {{row.department}}"
Reference: Supported Generator Types¶
local_group¶
Creates LocalGroup objects. When add_membership: true, calls add_group() on
the primary source object (e.g. a LocalUser).
generators:
dept_groups:
generator_type: local_group
source_field: department
unique_id_template: "dept_{{value}}"
add_membership: true
local_role¶
Creates LocalRole objects. When add_membership: true, calls add_role() on the
primary source object with apply_to_application=True.
generators:
title_roles:
generator_type: local_role
source_field: job_title
unique_id_template: "role_{{value}}"
add_membership: true
Tips and Edge Cases¶
Deduplication¶
Generated objects are deduplicated automatically. The runner looks up each rendered
unique_id in the provider before creating — if it already exists it reuses it.
No configuration is needed.
Normalising values before generation¶
If your source data has inconsistent casing (e.g. "engineering" vs "Engineering"),
apply a lowercase transform to source_field in field_mapping before the
generator reads it:
field_mapping:
department:
destination_field: department
transforms:
- name: lowercase
generators:
dept_groups:
generator_type: local_group
source_field: department # reads the lowercased value
unique_id_template: "dept_{{value}}"
Skipping empty values¶
skip_empty: true (the default) skips any record where source_field is blank or
null, preventing objects with empty names from being created. Set skip_empty: false
only if you intentionally want to create an object for an empty/zero value.
source_field naming¶
After field transforms have run, source_field in a generator should match the
destination field name from your field_mapping, not the original column name
(unless they are the same). In the example above, source_field: department matches
the destination_field: department entry in field_mapping.
If the generator’s source_field is not found in field_mapping, a warning is
logged but the runner will still attempt to read the field directly from the raw data
stream (useful for passthrough columns with no explicit mapping).
Generators with add_membership: false¶
Set add_membership: false to create groups/roles without assigning membership. This
is useful when you want to pre-populate groups for later assignment via a hook or
another source.
generators:
all_departments:
generator_type: local_group
source_field: department
add_membership: false # create groups but don't assign users
Advanced: API Sources with List Fields and Link-Only Mode¶
Some REST APIs return user objects where role or group membership is already expressed as a list of IDs or unique keys on the user record itself. For example, a Freshservice-style API might return:
{"email": "alice@example.com", "role_ids": [21, 22]}
Two generator options handle this pattern together:
multi_value: true— tells the runner thatsource_fieldholds a Python list; each element is processed as an independent value.create_if_missing: false— link-only mode; the runner looks up an existing OAA object by renderedunique_idand assigns membership only. Objects that are not found are skipped with a warning. This is appropriate when the roles/groups are declared in a separate source that runs first.
Example: pre-created roles + per-user role IDs from an API¶
version: v2
app:
name: My Service Desk
application_type: Freshservice
sources:
# Declare roles first so they exist before users are processed
roles:
destination: LocalRole
source_type: rest_api
connection: freshservice
params:
resource: roles
field_mapping:
id:
destination_field: unique_id
name:
destination_field: name
# Users carry their own role_ids list
users:
destination: LocalUser
source_type: rest_api
connection: freshservice
params:
resource: users
field_mapping:
email:
destination_field: unique_id
role_ids:
destination_field: role_ids
generators:
user_roles:
generator_type: local_role
source_field: role_ids # Python list from the API
unique_id_template: "{{value}}"
multi_value: true # iterate each element
create_if_missing: false # link only — roles already created above
add_membership: true
List elements that are objects (sub-field access)¶
The example above assumes each element of the list is a plain scalar (an integer ID). Some APIs return list fields where each element is itself an object — for example:
{"email": "alice@example.com", "scopeRoles": [{"roleId": 42, "roleName": "Admin"}]}
When multi_value: true and the elements are objects, use dot notation in
your templates to access sub-fields: {{value.fieldname}}. The {{value}}
variable is the full element object, not a string.
generators:
scope_role_assignments:
generator_type: local_role
source_field: scopeRoles # list of objects, not plain scalars
name_template: "{{value.roleName}}"
unique_id_template: "{{value.roleId}}"
multi_value: true
create_if_missing: false # roles already declared in a separate source
add_membership: true
skip_empty: true
This is distinct from the scalar case where {{value}} is sufficient. If your
template produces an empty string (e.g. {{value.roleName}} when the field is
missing), skip_empty: true will discard that element.
Source ordering matters for create_if_missing: false¶
When create_if_missing: false, the OAA objects must already exist in the
provider before the generator runs. Sources are processed in the order they are
declared in config.yaml, so declare the source that creates the objects
(roles, groups) before the source whose generator references them.
If a generator’s unique_id cannot be found in the provider, the runner logs
a warning and skips that value — no object is created and no membership is
assigned.