Configuration Schema Reference¶
The config.yaml file is the primary way to configure an OAA Runner integration.
This reference documents every supported field, its type, whether it is required,
and what it does.
For a conceptual overview see What is an OAA Config File?.
Top-level fields¶
Example:
version: v2
app:
name: My Application
application_type: custom
url: https://myapp.example.com
datasource: my-app-ds
sources:
users:
destination: LocalUser
source_type: custom
connection: my_api
params:
resource: /v1/users
field_mapping:
id:
destination_field: unique_id
email:
destination_field: email
permission_to_veza_map:
read:
permissions:
- DataRead
write:
permissions:
- DataWrite
- DataRead
run_params:
dry_run: true # parse data but don't push to Veza
save_json: false # save payload JSON to local file
no_push: false # skip all Veza API calls entirely
priority_push: false # push with priority flag
connections:
my_api:
connection_type: custom
custom_module: my_api
required_env:
- MY_API_KEY
Field |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
|
No |
|
Config format version. Always |
|
|
Yes* |
— |
Application definition. Required when |
|
|
No |
|
Runner type. Currently only |
|
|
No |
env var |
Base URL of your Veza instance (e.g. |
|
|
No |
env var |
Veza API key. Can be set via environment variable. |
|
|
No |
— |
Path to a PNG or SVG icon (≤ 64 KB) used for the provider in Veza. |
|
|
No |
|
Enable multipart upload for large payloads. |
|
|
No |
— |
Runtime execution flags ( |
|
|
No |
|
Label used for employee group types (HRIS integrations). |
|
|
No |
— |
Provider template to use: |
|
|
No |
|
IdP provider type ( |
|
|
No |
— |
Path to a directory containing custom logic modules. Added to |
|
|
No |
|
Python modules to load and run as post-processing logic hooks. |
|
|
No |
|
Global common hooks — framework-owned, reusable behaviors enabled by name, applied across the whole run. See Common hooks. |
|
|
No |
|
Named data sources. Keys are source names used throughout the config. |
|
|
No |
— |
Named connections to data systems. Keys are referenced by |
|
|
No |
|
Maps application-specific permission names to Veza permission types. |
app¶
Defines the application created in Veza.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
Display name of the application in Veza. |
|
|
No |
Application type identifier. |
|
|
No |
Domain (IdP integrations). |
|
|
No |
IdP type (IdP integrations). |
|
|
No |
HRIS type (HRIS integrations). |
|
|
No |
URL of the application. |
|
|
No |
Application-specific options. |
|
|
No |
Whether the application supports provisioning. |
|
|
No |
— |
|
|
No |
— |
|
|
No |
|
app:
name: My Application
application_type: custom
url: https://myapp.example.com
options:
enable_sso: true
provisioning: true
external_lifecycle_management_type: SCIM
datasource: my-app-ds
sources¶
Each key under sources is a named source. The value is a Source object.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
One of |
|
|
No |
Veza object type to create: |
|
|
Yes* |
Required when |
|
|
Yes* |
Required when |
|
|
No |
Human-readable name. Auto-set to the key name if omitted. |
|
|
No |
For |
|
|
No |
Name of the connection to use (must match a key under |
|
|
Yes* |
Path to the CSV file. Required when |
|
|
No |
Veza provider ID. Required for |
|
|
Yes* |
Path to a CSV field mapping file. Required when |
|
|
No |
Inline field mappings (preferred over |
|
|
No |
Resource type label for |
|
|
No |
SQL query (used with |
|
|
No |
Hierarchical resource configuration. |
|
|
No |
Arbitrary key/value parameters passed to the connection’s |
|
|
No |
Transform function names applied per-record via the stream pipeline. |
|
|
No |
Transform function names applied per-record outside the stream pipeline. |
|
|
No |
Declarative PETL transforms applied to the raw fetched stream, in order, before any PRE_TRANSFORM hooks run. See Reshaping Data with stream_transforms. |
|
|
No |
Common hooks scoped to this source. See Common hooks. |
|
|
No |
Stream this source’s fetched rows to a temporary SQLite file instead of caching them in RAM. Use for very large |
|
|
No |
Auto-register custom permissions for |
|
|
No |
Static attributes added to every record. |
|
|
No |
— |
Examples by source type:
# CSV source
sources:
users:
destination: LocalUser
source_type: csv
csv_filepath: ./data/users.csv
field_mapping:
employee_id:
destination_field: unique_id
email:
destination_field: email
# API source
sources:
users:
destination: LocalUser
source_type: api
connection: my_api
params:
resource: /v1/users
field_mapping:
id:
destination_field: unique_id
# DB source
sources:
users:
destination: LocalUser
source_type: db
connection: my_db
query: SELECT id, email, display_name FROM users
# CUSTOM source
sources:
users:
destination: LocalUser
source_type: custom
connection: my_custom
params:
resource: /users
# SOURCE (derived) — filters/extends another source
sources:
admins:
source_type: source
source: users
attributes:
role: admin
field_mapping entry (FieldMapping)¶
Each key in field_mapping is the source field name. The value can be:
A string — treated as the
destination_field(shorthand)A dict without
destination_field— the key is used asdestination_field(shorthand)A full dict — explicit
destination_fieldand other options
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes* |
Veza field this source field maps to. *Required in full syntax; auto-inferred from key in shorthand forms. |
|
|
No |
Override the source field name (defaults to the map key). |
|
|
No |
When |
|
|
No |
Modern form. Creates a Veza custom property with this type. Values: |
|
|
No |
Legacy. Use |
|
|
No |
Legacy. Use |
|
|
No |
List of transform functions to apply to the value. |
Shorthand syntax (recommended for simple mappings):
field_mapping:
# String shorthand — destination_field = "unique_id"
id: unique_id
# Dict shorthand — destination_field inferred as "department"
department:
source_field: dept
custom_type: STRING
# Empty dict — destination_field inferred as "email"
email: {}
Full syntax (needed for transforms or explicit control):
field_mapping:
user_id:
destination_field: unique_id
job_title:
destination_field: title
is_custom: true
veza_field_type: string
status:
destination_field: account_status
transforms:
- name: lowercase
Both syntaxes can be mixed within the same field_mapping.
⚠️ Important: Duplicate Field Mapping Keys¶
YAML silently uses the LAST definition when the same source field key appears twice in field_mapping. This is a YAML parsing behavior, not a validation error, and results in silent mapping failures.
Incorrect (second firstName overwrites first):
field_mapping:
firstName:
destination_field: first_name
firstName: # ❌ YAML ignores the above entry
destination_field: name
Correct (use aliases for multiple transforms on same source):
field_mapping:
firstName_raw:
source_field: firstName
destination_field: first_name
firstName_upper:
source_field: firstName
destination_field: name
transforms:
- name: uppercase
Or use Jinja2 templates in a single entry:
field_mapping:
firstName:
destination_field: first_name
full_name:
transforms:
- name: jinja_render
parameters:
template: "{{row.firstName}} {{row.lastName}}"
destination_field: name
Always verify that each unique source field appears exactly once as a key in your field_mapping.
Derived Sources (source_type: source)¶
When you need to create a new entity type from data already in another source (e.g., extract roles from users, derive departments from employee records), use source_type: source instead of making a second API call. This pattern reuses the parent source’s streaming data, avoiding redundant API requests and memory overhead.
Basic Pattern¶
sources:
users:
destination: LocalUser
source_type: custom
connection: myapp
params:
resource: /api/users
field_mapping:
id:
destination_field: unique_id
email:
destination_field: email
dept:
destination_field: department
# Derived source: extract roles from users
roles:
destination: LocalRole
source_type: source # ← Key: derives from users source
source: users # ← Parent source name
field_mapping:
role_name:
destination_field: name # Map from user data
role_id:
destination_field: unique_id
Critical Rules¶
1. Parent source must be listed BEFORE dependent source
The source that is referenced (users) must appear before the source that derives from it (roles). OAA Runner loads sources in order, so dependents cannot reference a parent that hasn’t loaded yet.
2. Do NOT include connection: or params: on dependent source
Dependent sources do not fetch data. They receive the parent’s streaming PETL pipeline after field mappings have run. No credentials or API parameters are needed.
3. Dependent source receives parent’s transformed data
The dependent source gets the parent source’s data AFTER field mapping transforms have already been applied. Use the destination field names, not original source names:
sources:
users:
source_type: custom
connection: myapp
field_mapping:
firstName:
destination_field: first_name # ← Transformed name
lastName:
destination_field: last_name # ← Transformed name
# Roles reads from transformed user data
roles:
source_type: source
source: users
field_mapping:
first_name: # ← Uses destination_field name
destination_field: created_by
Reshaping Data with PRE_TRANSFORM¶
If you need to extract, filter, or reorganize records from the parent stream, use a PRE_TRANSFORM hook targeting the dependent source:
from oaa.hooks import hook, OAAHookEvent
@hook(event=OAAHookEvent.PRE_TRANSFORM, target="roles")
def extract_roles_from_users(source=None, **kwargs):
"""Extract unique roles from user department field."""
def gen_roles():
seen = set()
for record in source.stream.dicts():
role_name = record.get("department")
if role_name and role_name not in seen:
yield {"role_name": role_name, "role_id": f"role_{role_name}"}
seen.add(role_name)
source._stream = source.stream.rowmapmany(
lambda r: [{"role_name": r.get("department"),
"role_id": f"role_{r.get('department')}"}],
header=["role_name", "role_id"]
)
Important details:
Use
@hook(event=OAAHookEvent.PRE_TRANSFORM, target="roles")— thetarget=kwarg scopes the hook to the “roles” sourceAccept
source=Noneas a parameter (NOTtable=None)Assign to
source._stream— no return valueTransform happens BEFORE field_mapping, so you work with the parent’s original field names, not destination names
Reshaping Data with stream_transforms¶
For reshaping that can be expressed as a fixed PETL pipeline, prefer the declarative stream_transforms list over a Python hook. Each entry is a single-key mapping: the key is the operation name and the value is the operation payload.
sources:
users:
destination: LocalRole
source_type: custom
connection: myapp
stream_transforms:
# Explode each user's groups into one row per group
- rowmapmany:
iterable:
field: row.userGroups
skip_if: item.id is none
columns:
role_id: item.id
role_name: item.name
role_owner: row.ownerUserId
# De-duplicate on role_id (also sorts by role_id)
- distinct: [role_id]
# Keep only rows whose owner is set
- select: row.role_owner
# Reorder rows by role_name
- sort: [role_name]
# Drop fields that are not needed downstream
- cutout: [raw_groups]
stream_transforms run on the raw fetched stream before any PRE_TRANSFORM hooks, so hooks always see the reshaped data.
Supported operations:
Op |
Payload |
Description |
|---|---|---|
|
|
One output row per input row. See below. |
|
|
Explode each input row into zero or more output rows (one per item in |
|
field name / list |
Remove duplicate rows on the given key. PETL |
|
field name / list |
Sort rows by the given fields. |
|
Jinja expression |
Keep only rows where the expression is truthy ( |
|
field name / list |
Keep only the listed fields. |
|
field name / list |
Drop the listed fields. |
|
integer |
Keep only the first N rows. |
RowmapConfig:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes* |
The array (or delimited string) to explode. Required for |
|
|
Yes |
Output column → Jinja expression. Header order follows the dict insertion order. |
|
|
No |
Jinja expression; rows (or items) matching it are dropped. |
RowmapIterable:
Field |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
|
Yes |
— |
Jinja expression resolving to the iterable, e.g. |
|
|
No |
|
|
|
|
No |
|
Delimiter used when |
|
|
No |
|
Strip whitespace from items when |
Expressions are Jinja expressions (no {{ }} braces) and preserve native types: an integer stays an integer, a list stays a list, and a missing value resolves to None. Rows are passed as PETL Record objects, so row.field works for most field names; field names that collide with a Record method (e.g. count) need subscript access (row['count']).
These transforms are lazy — the stream is only consumed as downstream processing pulls it.
Why Not a Second API Call?¶
Making a second fetch() call instead of using source_type: source has several downsides:
Problem |
Impact |
|---|---|
Redundant credentials |
Requires separate auth, potentially failing if credentials don’t work twice |
Memory overhead |
Both sources load independently into memory instead of streaming together |
Sync issues |
Data inconsistencies between two separate API calls (e.g., if API state changes mid-run) |
Performance |
Each API call has latency, multiplied by pagination overhead |
Complexity |
More code to maintain, more error handling needed |
Real-World Example: BongaWonga Users → Roles¶
sources:
users:
destination: LocalUser
source_type: custom
connection: bongawonga_api
params:
resource: /api/users
field_mapping:
id:
destination_field: unique_id
email:
destination_field: email
name:
destination_field: name
role_name:
destination_field: assigned_role
roles:
destination: LocalRole
source_type: source
source: users
field_mapping:
assigned_role:
destination_field: name
assigned_role_id: # Note: different key to avoid duplicate
destination_field: unique_id
With a PRE_TRANSFORM hook to extract unique roles:
@hook(event=OAAHookEvent.PRE_TRANSFORM, target="roles")
def extract_unique_roles(source=None, **kwargs):
"""Extract unique role names from user records."""
def gen_roles():
seen = set()
for record in source.stream.dicts():
role = record.get("assigned_role")
if role and role not in seen:
yield {"assigned_role": role}
seen.add(role)
# Reshape stream to only contain one record per unique role
source._stream = source.stream.rowmapmany(
lambda r: [{"assigned_role": r.get("assigned_role")}] if r.get("assigned_role") and r.get("assigned_role") not in set() else [],
header=["assigned_role"]
)
tree¶
Beta: Tree functionality is still in beta and may change in future releases.
Used when a source represents a hierarchical resource (e.g. org chart).
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
Field name containing the node’s own ID. |
|
|
Yes |
Field name containing the parent node’s ID. |
|
|
No |
Name to use for the child list. Defaults to |
sources:
org_chart:
destination: Resource
resource_type: Department
source_type: custom
connection: my_api
tree:
id_field: id
parent_id_field: manager_id
sub_fieldname: direct_reports
generators¶
Declares one or more inline object generators on a source. Each generator
reads a field from every source record and automatically creates LocalGroup
or LocalRole objects from the unique values it finds — optionally assigning
membership at the same time.
Generators are deduplicated automatically: if 50 user records all share the same field value, exactly one group or role is created.
For a full walk-through see How to Use Generators.
Each key under generators is an arbitrary generator name. The value is:
Field |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
|
Yes |
— |
Type of object to create: |
|
|
Yes |
— |
Field name to read values from. Must match the destination field name after |
|
|
No |
|
Jinja2 template for the generated object’s |
|
|
No |
|
Jinja2 template for the generated object’s |
|
|
No |
|
When |
|
|
No |
|
Skip records where |
|
|
No |
|
Treat |
|
|
No |
|
When |
|
|
No |
— |
Optional extra property mappings to set on the generated object. Only |
Minimal example — only required fields, all other keys take their defaults:
sources:
users:
destination: LocalUser
source_type: csv
csv_filepath: data/users.csv
field_mapping:
department:
destination_field: department
generators:
dept_groups:
generator_type: local_group
source_field: department
Fully-specified example — all keys explicit:
sources:
users:
destination: LocalUser
source_type: csv
csv_filepath: data/users.csv
field_mapping:
department:
destination_field: department
job_title:
destination_field: job_title
generators:
dept_groups:
generator_type: local_group
source_field: department
name_template: "{{value}}"
unique_id_template: "dept_{{value}}"
add_membership: true
skip_empty: true
multi_value: false
create_if_missing: 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
Link-only mode with a list field — roles already created by a separate source; generator only wires up per-user membership:
sources:
roles:
destination: LocalRole
source_type: custom
connection: api
params:
resource: /roles
field_mapping:
id:
destination_field: unique_id
name:
destination_field: name
users:
destination: LocalUser
source_type: custom
connection: api
params:
resource: /users
field_mapping:
id:
destination_field: unique_id
role_ids:
destination_field: role_ids
generators:
user_roles:
generator_type: local_role
source_field: role_ids
unique_id_template: "{{value}}"
multi_value: true # role_ids is a list
create_if_missing: false # roles already exist from the roles source above
add_membership: true
Source ordering with
create_if_missing: false: Sources are processed in the order they are declared. When using link-only mode, the source that creates the target objects must be declared before the source whose generator references them.
connections¶
Each key under connections is a named connection. The value’s shape depends on connection_type.
All connection types support the required_env field:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
No |
List of environment variable names that this connection requires. oaa-runner validates that each variable is set before running, and auto-loads them from |
CSV connection¶
connections:
my_csv:
connection_type: csv
No additional fields required. The file path is set on the source via csv_filepath.
API connection¶
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
|
|
|
Yes |
Base URI of the API. |
|
|
Yes |
Python module that implements |
|
|
No |
HTTP method: |
|
|
No |
Default query parameters. |
|
|
No |
Authentication parameters passed to |
|
|
No |
Maximum number of records to fetch. |
|
|
No |
Environment variables required by this connection. |
connections:
my_api:
connection_type: api
uri: https://api.example.com
fetch_module: modules.myapp
method: GET
required_env:
- MY_API_KEY
- MY_API_SECRET
auth:
api_token: "!env"
Veza API connection¶
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
|
|
|
Yes |
Veza API path to query (e.g. |
|
|
No |
Expected response shape: |
|
|
No |
Maximum number of records to fetch. |
|
|
No |
Environment variables required by this connection. |
connections:
veza:
connection_type: veza_api
path: /api/v1/resources
response_type: list
max_results: 1000
DB connection¶
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
|
|
|
Yes |
Database type: |
|
|
Yes |
Hostname or IP of the database server. |
|
|
Yes |
Database name. |
|
|
Yes |
Database username. Use |
|
|
Yes |
Database password. Use |
|
|
No |
Database port. |
|
|
Yes* |
SQL query string. Required if |
|
|
Yes* |
Path to a |
|
|
Yes* |
Named query streams. Required if |
|
|
No |
Environment variables required by this connection. |
connections:
my_db:
connection_type: db
DB_DIALECT: SQLSERVER
DB_SERVER: db.example.com
DB_NAME: mydb
DB_USER: sa
DB_PASSWORD: "!env"
QUERY_PATH: ./queries/users.sql
Custom connection¶
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
|
|
|
Yes |
Python module name that implements |
|
|
No |
Optional query string passed to the module. |
|
|
No |
Arbitrary parameters passed to |
|
|
No |
Environment variables required by this connection. |
connections:
my_custom:
connection_type: custom
custom_module: modules.myapi
required_env:
- MY_API_KEY
- MY_API_SECRET
params:
base_url: https://api.example.com
Common connection¶
No-code connection type. Delegates all fetching to the concurrent_fetch built-in hook. No fetch_module required.
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
|
|
|
Yes |
List of built-in hook configs. Must include |
|
|
No |
Environment variables required by this connection. |
connections:
api:
connection_type: common
required_env: [MY_CLIENT_ID, MY_CLIENT_SECRET]
common_hooks:
- name: concurrent_fetch
options:
base_url: "https://api.example.com"
auth:
type: oauth_client_credentials
client_id: "${MY_CLIENT_ID}"
client_secret: "${MY_CLIENT_SECRET}"
See how-to-enable-common-hooks.md for full options.
permission_to_veza_map¶
Maps application permission names to Veza permission types.
permission_to_veza_map:
read:
permissions:
- DataRead
write:
name: Write Access
permissions:
- DataWrite
- DataRead
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
No |
Display name for this permission mapping. |
|
|
Yes |
List of Veza |
Migration note: Earlier versions accepted a bare list as the value (e.g.
read: [DataRead]). This format is no longer supported. Wrap the list in apermissionskey as shown above. oaa-runner will raise a clear error if the old format is detected.
Valid Veza Canonical Permissions¶
Admin is not a valid permission value. Use the full permission set:
Permission |
Purpose |
|---|---|
DataRead |
Read/view data |
DataWrite |
Modify data |
DataCreate |
Create new data |
DataDelete |
Delete data |
MetadataRead |
Read metadata/configuration |
MetadataWrite |
Modify metadata |
MetadataCreate |
Create new metadata items |
MetadataDelete |
Delete metadata items |
Correct format (with permissions: wrapper):
permission_to_veza_map:
Admin:
permissions:
- DataRead
- DataWrite
- DataCreate
- DataDelete
- MetadataRead
- MetadataWrite
- MetadataCreate
- MetadataDelete
Editor:
permissions:
- DataRead
- DataWrite
Viewer:
permissions:
- DataRead
Incorrect (bare list, no permissions: wrapper):
permission_to_veza_map:
Admin: # ❌ Missing permissions: wrapper
- DataRead
Incorrect (Admin value):
permission_to_veza_map:
Admin:
permissions:
- Admin # ❌ Not a valid permission value
run_params¶
Runtime execution flags that control how a single run behaves. These can also be overridden by CLI arguments (CLI takes precedence).
run_params:
dry_run: false # parse data but don't push to Veza
save_json: false # save payload JSON to local file
no_push: false # skip all Veza API calls entirely
priority_push: false # push with priority flag
Field |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
|
No |
|
Parse data but don’t push to Veza. |
|
|
No |
|
Save payload JSON to a local file. |
|
|
No |
|
Skip all Veza API calls entirely. |
|
|
No |
|
Push with priority flag. |
Common hooks¶
common_hooks enable framework-owned, reusable behaviors by name —
the same idea as transforms, but for whole-stream / lifecycle / fetch behavior.
Each entry is a CommonHook: either a bare hook name (string) or a
name + options mapping.
common_hooks:
- filter_stream_by_field # shorthand
- name: filter_stream_by_field # with options
options:
field: Trustee_Type
operator: eq
value: "1"
action: keep
Declaration levels¶
A common hook can be declared at three levels, which set what it is scoped to:
Level |
Where |
Scope |
|---|---|---|
Global |
top-level |
the whole run (every source) |
Source |
|
that one source |
Connection |
|
every source that uses the connection |
Lifecycle hooks bind to a lifecycle event (e.g. PRE_TRANSFORM) that the hook
itself declares — config only enables the hook and supplies options.
Built-in common hooks¶
Name |
Phase |
Event |
Description |
|---|---|---|---|
|
fetch |
— |
Concurrent offset-paginated fetch. Declared on a connection (or source). See How to Enable Common Hooks. |
|
lifecycle |
|
Resolve a secret from an external credential store into an |
|
lifecycle |
|
Keep or drop source rows by a field value. Options: |
|
lifecycle |
|
Prints a message per scoped source — a trivial hook for smoke-testing common-hook wiring. Option: |
|
push |
— |
Memory-bounded push for large populations: serializes the provider to SQLite one object at a time, frees the in-memory graph, then streams the payload from disk into a multipart upload — avoiding the full-dict + full-JSON-string peak of the default push. Declared globally. Options: |
# Source-scoped filter: keep only rows where kind == "user"
sources:
users:
source_type: custom
connection: api
common_hooks:
- name: filter_stream_by_field
options:
field: kind
operator: eq
value: user
action: keep
Required environment variables¶
The required_env field on any connection explicitly declares which environment
variables are required for that connection. It has three behaviors:
Validation — Before running, oaa-runner checks that every listed variable is set in the environment (or
.envfile). If any are missing, the run fails with a clear error naming the variable and the connection that declared it.Auto-loading —
.envfiles are loaded from the same directory as theconfig.yamlfile. Whenrequired_envis used, integration modules no longer need to callload_dotenv()themselves.Injection — Values for listed variables are automatically injected into
connection.params, so integration code can access them asconnection.params['MY_API_KEY']instead of callingparams_or_env().
VEZA_URL and VEZA_API_KEY are always implicitly required regardless of
whether required_env is present — they do not need to be listed explicitly.
connections:
my_api:
connection_type: custom
custom_module: modules.myapp
required_env:
- MY_API_KEY
- MY_API_SECRET
params:
base_url: https://api.example.com
With the above config and a .env file containing MY_API_KEY=abc123, the
integration’s fetch() function receives connection.params as:
{
"base_url": "https://api.example.com", # from config
"MY_API_KEY": "abc123", # auto-loaded from .env
"MY_API_SECRET": "def456", # auto-loaded from .env
}
Environment variable substitution¶
There are two ways to pull environment values into the config, plus the
required_env injection described above.
${VAR} interpolation (config-wide)¶
Any string value anywhere in config.yaml may reference an environment
variable with ${VAR} syntax. References are resolved once, at config-load
time, across the entire file — app, sources, connections, hook options,
everything:
app:
name: dcps-${ENVIRONMENT_NAME}
application_type: dcps-${ENVIRONMENT_NAME}
connections:
api:
connection_type: common
required_env: [DCPS_INSTANCE, DCPS_CLIENT_ID, DCPS_CLIENT_SECRET]
common_hooks:
- name: concurrent_fetch
options:
base_url: "https://${DCPS_INSTANCE}"
auth:
type: oauth_client_credentials
client_id: "${DCPS_CLIENT_ID}"
client_secret: "${DCPS_CLIENT_SECRET}"
Behavior:
.envfiles are loaded automatically (from both the current directory and theconfig.yamldirectory) before interpolation, so variables defined there are available. Real environment variables take precedence over.env.If a referenced variable is not set, the
${VAR}text is left unchanged and a warning is logged — a non-env${...}literal is never silently blanked out.A
${VAR}substring is replaced in place, sodcps-${ENVIRONMENT_NAME}becomesdcps-prod.
Use required_env on a connection (below) when you want a run to fail fast
if a variable is missing rather than fall back to the literal.
!env substitution (field-name match)¶
Fields with the exact value !env are replaced at runtime with the value of
the environment variable whose name matches the field name:
connections:
my_db:
connection_type: db
DB_DIALECT: SQLSERVER
DB_USER: sa
DB_PASSWORD: "!env" # reads DB_PASSWORD from environment
This differs from ${VAR}: !env matches the field’s own name and replaces
the whole value, whereas ${VAR} can name any variable and can be embedded
inside a larger string.
For connections with required_env, values are additionally injected into
connection.params — see
Required environment variables.
Validating your config¶
Before running a sync, validate the config file:
oaa app validate --config=config.yaml
# Also test live connectivity to data sources:
oaa app validate --config=config.yaml --test-connectivity