Documentation menu

"Example: flatRoles → Functions (v1 migration)"

Migrate v1 flatRoles truthfully: claims via Functions, with optional best-effort Atrium department group sync.

The old v1 flatRoles script did two real things:

  1. emitted claims from persisted user metadata and grants
  2. indirectly proved that department existed on the ZITADEL user

It did not prove that login Event payloads carry department inline.

What the v2 replacement actually is

This migration belongs on Functions, not Events.

Use:

  • preuserinfo when consumers read the claim from userinfo / ID-token-adjacent flows
  • preaccesstoken when consumers read the claim from the access token
  • both when both surfaces matter

In OMMAX, the canonical template pair is:

  • flatten-roles
  • flatten-roles-access-token

What parity means here

Feature parity with v1 means preserving:

  • urn:ommax:department
  • litellm_team_ids
  • my:zitadel:grants

and, in the upgraded OMMAX version, optionally also:

  • best-effort sync into the matching Atrium department group

That side effect must never be allowed to become more important than the claim mutation itself.

Payload reality

The observed v2 function payloads are function-shaped:

  • payload.user
  • payload.userinfo
  • payload.user_metadata
  • payload.user_grants
  • and, for access-token flows, sometimes payload.accessToken.sub

So the safe pattern is:

  1. resolve userId
  2. read metadata from user_metadata if present
  3. fall back to ctx.zitadel.users.getMetadata(...)
  4. build setClaims
  5. if desired, do best-effort Atrium group sync inside a try/catch
  1. Activate flatten-roles on FUNCTION:preuserinfo
  2. Activate flatten-roles-access-token on FUNCTION:preaccesstoken if the same claims are needed in access tokens
  3. Compare the emitted claims against the old v1 result for one test user
  4. Only then disable the old v1 action

Why not use an Event for this?

Because flatRoles is claim-setting logic.

The right split is:

  • Functions for claim mutation
  • Events for after-the-fact lifecycle work

If you also want department-based Atrium groups, you have two honest options:

  • fold best-effort group sync into the Function templates
  • or add a separate Event script for lifecycle/login automation

Which one you choose depends on whether your real need is:

  • claims parity
  • onboarding
  • login-time catch-up
  • or all of them

Reference implementation

The authoritative reference sources in this repo are:

The templates surfaced in /admin/actions are kept in sync with those references by test coverage.