Mapping Language
Auth0 Relationship Sync uses a mapping file to describe how Auth0 events become FGA tuples. A mapping file is a small, declarative YAML document: it matches incoming Auth0 events against rules, pulls out the fields you care about, and produces FGA tuple writes or deletes. This page is the complete syntax reference for that mapping file.
Here's the basic shape: two rules that add and remove a user from an organization as Auth0 emits membership events.
Structure
version: "1"
rules:
- name: "Add user to organization"
when: input.type == "organization.member.added"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuples:
- user: "user:{{ variables.user_id }}"
relation: "member"
object: "organization:{{ variables.organization_id }}"
- name: "Remove user from organization"
when: input.type == "organization.member.deleted"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuples:
- action: delete
user: "user:{{ variables.user_id }}"
relation: "member"
object: "organization:{{ variables.organization_id }}"
Rule fields
| Field | Required | Description |
|---|---|---|
name | yes | Label shown in validation output and error messages. |
when | no | Expr expression. The rule is skipped if false. Supports &&, ||, !, comparisons, and all functions listed below. |
variables | no | Ordered map of name → Expr. Each variable can reference input and any previously defined variable in the same rule. |
tuples | yes (unless every tuple_filters entry deletes) | One or more tuple templates to write or delete. Not required when tuple_filters is present and every filter's action is delete; otherwise required, since a filter that patches (the default action) needs tuples to compute the desired state. |
tuple_filters | no | Read-diff-write or full-delete against existing FGA tuples. See Tuple filters. |
iterator | no | Fan-out: evaluates tuples once per item in a collection. |
Tuple fields
| Field | Required | Description |
|---|---|---|
user | yes | Interpolated string for the user URN. Use {{ variables.name }}, {{ input.field }}, or the iterator alias. |
relation | yes | Interpolated string for the relation name. |
object | yes | Interpolated string for the object URN. |
action | no | "write" (default) or "delete". |
Idempotent writes and deletes
Every write or delete a mapping engine performs ignores conflicts rather than failing: writes pass OpenFGA's on_duplicate: "ignore" (skip a tuple that already exists), and deletes pass on_missing: "ignore" (skip a tuple that's already gone). This applies automatically to every tuple template; there's nothing to configure in the mapping. It's what makes it safe for a rule to write or delete the same tuple more than once, whether from a genuinely duplicate event or from reprocessing during reconciliation.
See Ignoring duplicate or missing tuples for how these options work at the OpenFGA API level.
Tuple filters
Tuple filters power two patterns:
Reconcile (read-diff-write): the mapping engine reads tuples matching the filter from FGA, computes the delta against the desired state in tuples, and writes the minimum changes. Use this for update events where the source system sends the full new state but not what specifically changed:
- name: "Update organization plan"
when: >-
input.type == "organization.updated" &&
input.data.object.metadata.subscription_plan != nil
variables:
organization_id: input.data.object.id
subscription_plan: input.data.object.metadata.subscription_plan
tuple_filters:
- relation: subscription_plan
object: "organization:{{ variables.organization_id }}"
tuples:
- user: "plan:{{ variables.subscription_plan }}"
relation: "subscription_plan"
object: "organization:{{ variables.organization_id }}"
Full delete: set action: delete to remove all tuples matching the filter, without writing replacements:
- name: "Delete all organization tuples on deletion"
when: input.type == "organization.deleted"
variables:
organization_id: input.data.object.id
tuple_filters:
- object: "organization:{{ variables.organization_id }}"
action: delete
Cleaning up roles on membership removal
Auth0 emits organization.member.deleted when a user leaves an organization, or when the organization itself is deleted, but never emits organization.member.role.deleted in either case. Left alone, a user's role-assignment tuples outlive their membership. Add a rule that filters on the user and organization without pinning a relation, so it catches whatever role tuples exist:
- name: "Delete organization member roles on membership removal"
when: input.type == "organization.member.deleted"
variables:
organization_id: input.data.object.organization.id
user_id: input.data.object.user.user_id
tuple_filters:
- user: "user:{{ variables.user_id }}"
object: "organization:{{ variables.organization_id }}"
action: delete
This runs alongside your normal membership-removal rule (the one that deletes the member relation). Omitting relation here means it matches and deletes any relation this user has to this organization, including roles assigned as relations directly on the organization via organization.member.role.assigned. This technique assumes roles are modeled as relations on the organization itself. If your model instead writes role assignments to a separate, tenant-wide role:{id} object (as the comprehensive template does), a user+organization filter can't reach those tuples directly, since a role:{id} object isn't organization-scoped; that template wires role assignments through an intermediate organization_membership object instead, so a single delete on membership removal revokes every role tied to it without needing to enumerate role IDs. See the note under that template.
Cleaning up group tuples on group deletion
Auth0 emits only the single group.deleted event when a SCIM group is deleted. There's no fan-out to a group.member.deleted event per member, so a deleted group's membership tuples outlive the group. Two filters cover this, since the deleted group can appear on either side of a tuple:
- name: "Delete all tuples on group deletion"
when: input.type == "group.deleted"
variables:
group_id: input.data.object?.group?.id
tuple_filters:
- object: "group:{{ variables.group_id }}"
action: delete
- user: "group:{{ variables.group_id }}#member"
action: delete
The first filter matches every tuple where the deleted group is the object: its direct user members, any child groups nested inside it, and its connection relation tuple if your model has one. The second filter catches the case the first one misses: if the deleted group was itself nested as a member of some other group, that tuple has the deleted group in the user position (group:{{ variables.group_id }}#member), not the object, so it needs its own filter.
Tuple filter fields: object (optional, interpolated), user (optional, interpolated), relation (optional), action ("delete" to remove all matching; omit to reconcile against tuples).
Iterator (fan-out)
Emit one set of tuples per item in a collection. Useful when a single input document contains an array of entities:
- name: "Register identity providers on user creation"
when: input.type == "user.created" && len(input.data.object.identities) > 0
variables:
raw_user_id: input.data.object.user_id
user_id: replace(variables.raw_user_id, "|", ":")
iterator:
source: input.data.object.identities
as: identity
tuples:
- user: "user:{{ variables.user_id }}"
relation: "authenticated_via"
object: "connection:{{ identity.connection }}"
Delete actions
Delete can be set per-tuple (action: delete inside a tuple template); see the "Remove user from organization" example above.
Deleting nested/userset members uses the #relation suffix syntax:
- name: "Remove group member from group"
when: >-
input.type == "group.member.deleted" &&
input.data.object.member.member_type == "group"
variables:
group_id: input.data.object?.group?.id
member_id: input.data.object?.member?.id
tuples:
- action: delete
user: "group:{{ variables.member_id }}#member"
relation: "member"
object: "group:{{ variables.group_id }}"
Expression language
when guards and variable expressions use the Expr language. Expressions are sandboxed: no file I/O, network, or shell access.
Available context: input (the full input document), variables (values computed earlier in the same rule), and the iterator alias inside iterator rules.
String functions: lower(s), upper(s), trim(s), replace(s, old, new), split(s, sep), join(arr, sep), hasPrefix(s, prefix), hasSuffix(s, suffix), contains (infix operator: "hello" contains "ell").
Collection functions: len(x), in (membership: "x" in arr), ?? (null-coalescing: value ?? "default").
json_path(obj, path): safely traverses nested structures using a dot-separated path. Returns nil for missing paths instead of throwing.
json_path(input.data.object, "user.user_id") → "auth0|abc123"
json_path(input.data.object, "role.name") → "admin"
json_path(input.data.object, "missing.key") → nil
json_pathTwo-level key access like input.data.object.user.user_id throws if the intermediate key (user) is absent. Use json_path whenever the path may not exist across all input types processed by the same mapping file. Optional chaining (input.data.object?.app_metadata?.department_id) is the lighter-weight alternative when you just need a nil-safe read, not a dynamic path.
Embedded tests
Mapping files can include a tests: section with sample input documents and expected outputs:
tests:
- name: "organization.member.added writes member tuple"
input:
type: "organization.member.added"
data:
object:
organization:
id: "org_acme"
user:
user_id: "u_alice"
expect_tuples:
- user: "user:u_alice"
relation: "member"
object: "organization:org_acme"
Use expect_tuple_filters to assert what filter operations a rule produces:
expect_tuple_filters:
- object: "organization:org_acme"
action: delete
Running mappings from the command line
To validate, test, and run a mapping file locally, see Writing a Mapping File.