> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fga.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Mapping Language

> The complete mapping file syntax for Auth0 Relationship Sync, covering rules, tuple templates, tuple filters, iterators, and the expression 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

```yaml theme={null}
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](https://github.com/expr-lang/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. |
| `tuple_filters` | no | Read-diff-write or full-delete against existing FGA tuples. See [Tuple filters](#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](https://docs.fga.dev/integration/update-tuples.md#05-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:

```yaml theme={null}
- 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 }}"
```

Reconcile re-reads the current tuples on every update event, even when the field didn't change. If the event carries `data.previous_object`, you can skip no-op events and avoid the read; see [Attributes and metadata](https://docs.fga.dev/relationship-sync/use-cases/attributes-and-metadata.md#choosing-an-approach) for both approaches and their trade-offs.

**Full delete**: set `action: delete` to remove all tuples matching the filter, without writing replacements:

```yaml theme={null}
- 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:

```yaml theme={null}
- 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, a user+organization filter can't reach those tuples directly, since a `role:{id}` object isn't organization-scoped. See [Roles: a user, within an organization membership](https://docs.fga.dev/relationship-sync/use-cases/roles.md#a-user-within-an-organization-membership) for a pattern that wires role assignments through an intermediate object instead, so a pair of filters on membership removal revokes every role tied to it without needing to enumerate role IDs.

### 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:

```yaml theme={null}
- name: "Delete all tuples on group deletion"
  when: >-
    input.type == "group.deleted" &&
    input.data.object?.id != nil
  variables:
    group_id: input.data.object.id
  tuple_filters:
    - object: "group:{{ variables.group_id }}"
      action: delete
    - object: "group:"
      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:**

| Field | Required | Description |
| - | - | - |
| `object` | yes | At minimum a bare type prefix like `"group:"`, even if the filter is really keyed on `user`. |
| `user` | no | Interpolated string, same as a tuple template's `user`. |
| `relation` | no | Interpolated string, same as a tuple template's `relation`. |
| `action` | no | `"delete"` to remove all matching tuples; 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:

```yaml theme={null}
- 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:

```yaml theme={null}
- 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](https://github.com/expr-lang/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
```

> [!TIP]
>
> **When to use `json_path`**
>
> Two-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:

```yaml theme={null}
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:

```yaml theme={null}
    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 [Working with Mapping Files](https://docs.fga.dev/relationship-sync/mapping-files.md#using-the-cli).
