Tutorial: Building an Integration with the CLI

This tutorial walks through building a small OAA integration entirely with oaa CLI commands — scaffolding the project, inspecting what the framework offers, previewing and validating data, and finally checking the result into a Veza tenant — without writing a full sync loop by hand at any point along the way.

By the end, you’ll have a working integration skeleton for a fictional “BongaWonga” source, validated end-to-end against mocked data.

Prerequisites

  • oaa-runner installed (uvx oaa version should print a version number)

  • A terminal in an empty working directory

Step 1: Scaffold the project

Start with init, giving it a name and two source names:

oaa init bongawonga --source_names=employees,departments

This creates:

bongawonga/
  README.md
  config.yaml
  sample.env
  requirements.txt
  modules/
    bongawonga.py
    bongawonga_hooks.py
    bongawonga_mocks.py

cd into bongawonga/ for the rest of this tutorial.

Step 2: See what the framework offers

Before writing any mapping logic, look at what’s available to you. List the entity types you can map sources to:

oaa list_entity_types

Say your employees source should map to LocalUser. Check exactly which fields that requires:

oaa show_entity --entity_type=LocalUser

And check which transform functions you can reference from field_mapping:

oaa list_transforms

Step 3: Validate the config structure

Even before any source code is filled in, confirm the generated config.yaml is structurally valid:

oaa validate_config --config_file=./config.yaml

And confirm every ${VAR} your config references (check sample.env for the expected names) is set in your environment:

cp sample.env .env
# edit .env with real values, then export it, e.g.:
export $(grep -v '^#' .env | xargs)
oaa check_env --config_file=./config.yaml

Step 4: Preview data before mapping

With modules/bongawonga.py’s fetch() filled in (or left as the generated mock-backed stub), preview what it returns for the employees source:

oaa preview --source_name=employees --limit=5

Now edit config.yaml’s field_mapping for employees to map the fields you saw, and preview again — this time after mapping:

oaa preview_mapped --source_name=employees --limit=5

If a mapped field doesn’t show up the way you expect, run validate_mappings to check for a typo’d source field name:

oaa validate_mappings --config_file=./config.yaml

Step 5: Add a second source

Add the departments source’s connection details the same way, or use generate_source if you’re adding a source that wasn’t part of the original init:

oaa generate_source custom vendors --config_file=./config.yaml

This both adds the sources/connections blocks to config.yaml and creates modules/vendors.py from the standard template, ready for you to fill in fetch().

Step 6: Add a hook

Say you want to normalize a field right before it’s transformed. Scaffold a hook function targeting the employees source:

oaa generate_hook PRE_TRANSFORM --target=employees

This creates modules/pre_transform_employees.py with the @hook decorator already wired to OAAHookEvent.PRE_TRANSFORM. Fill in the function body, then confirm it’s registered:

oaa list_hooks

Step 7: Diff against a known-good config

If you’re iterating on config.yaml and want to see exactly what changed since your last known-good version:

cp config.yaml config.yaml.bak
# ... make edits ...
oaa diff_config --config_file=./config.yaml.bak --compare_file=./config.yaml

Step 8: Check the result in Veza

Once you’ve run a real sync (outside the scope of this tutorial — see oaa run), confirm the provider landed correctly:

oaa list_providers
oaa show_provider --name=bongawonga
oaa provider_status --name=bongawonga

If you need to start over, delete the provider (this requires an explicit confirm, since it’s destructive):

oaa delete_provider --name=bongawonga --confirm=True

End-to-End Integration Validation

Before committing a new integration, you might start with one of the bundled samples and validate it end-to-end:

oaa show_sample --name=custom-app-users-groups-roles
cd custom-app-users-groups-roles

Run static checks first (no API credentials needed):

oaa validate_integration --path=. --skip_mock=True

If that passes, run the full pipeline including a mock-data run:

oaa validate_integration --path=.

Inspect the resulting payload:

oaa run --save-json=./payload.json --no-push
oaa anonymize --input=./payload.json --output=./safe.json
oaa validate_payload --payload=./payload.json --config=./config.yaml

The anonymize step redacts sensitive data (if any were accidentally included), and validate_payload reports field presence, date formats, role permissions, relationships, and shape diffs between your configured mappings and the actual output.

If any step fails, parse_errors can help classify what went wrong:

oaa run --no-push 2>&1 | oaa parse_errors
oaa parse_errors --input=./run.log --category=field_mapping

Next steps