Skip to content
AI Portal Docs
On this page What happens on each request 1/11

A departmentDepartmentPhòng banThe bucket that pays: it owns a budget, limits and which models its people may use. Everyone belongs to exactly one.Open in glossary is the cost and policy bucket every request is billed to. Its budget and limits reach everybody billed there, except people given limits of their own — see Budgets and policy.

Key rules

  1. Every person is in exactly one department. There is no “no department”: unassigned people sit in GeneralGeneralGeneralThe built-in department for everyone not assigned to another one, so every request always has a department to bill.Open in glossary.
  2. The department is read on every request, from the person’s directory record — never from a sign-in token. A move applies on the next request, with no re-login.
  3. A group is never a department member. Assigning from a groupGroupNhómA set of people used to give roles and to name in sharing rules. A person can be in many groups; a group never holds money.Open in glossary copies its people once; editing the group later moves nobody.
  4. Directory mode decides who writes membership: the portal (PORTAL) or your identity provider (IDP).
  5. Moving someone moves their spend and can widen or narrow which models they may run.

What happens on each request

  1. The portal reads the person’s directory record. No record means no access.
  2. It takes their department — General if they were never assigned one.
  3. If that department blocks the chosen model, the turn is refused.
  4. Otherwise the call runs on the department’s key, or the person’s own if they hold a personal limit.
  5. The budget comes from the winning policy level, and the spend is billed to the department.

Paths into a department

PathHow it happens
Provisioned with no departmentThe person lands in the seeded General department
Single-person reassignmentAn admin moves one person (PATCH /users/:id)
Bulk assignment from a groupAn admin assigns a group’s members (POST /groups/:id/assign-department)
Attached provider groups, IDP mode onlyDepartment resolution, below

Every path writes the person’s own record; nothing reads a group at request time. Only an attached group in IDP mode moves people when its membership changes upstream.

Bulk assign with conflict review

A bulk assignment expands the group once and bills everybody not already in the target department to it. No membership is stored, so later group edits move nobody. The result names each person moved and the department they came from.

Bulk assign dialog listing people currently billed to another department
The conflict review names everyone who would leave another department.
The conflict review names everyone who would leave another department.

General and archived departments

  • General is a real department. Anybody in it is reported as General. Its Admin console page lists members and accepts new ones like any other, but offers no removal — removing someone from a department files them in General.
  • An archived department never receives anybody. No picker offers one, and an invitation naming one is refused when accepted.

Who owns which field

Access is gated by two fields with two owners, and both must allow.

FieldOwnerWritten by
email, displayName, status, external_idIdentity providerSCIM
A group’s name, external id and membership of a synced groupIdentity providerSCIM Groups
suspended_atPortalSuspend / unsuspend
department_id, org_role, membership of a manual groupPortalAdmin actions in PORTAL mode; department resolution in IDP mode
A group’s role grants and visibility rulesPortalAdmin actions, both modes
A department’s name, description, cost centre and budgetPortalAdmin actions, both modes

The department itself is always portal-owned: no identity provider carries a budget or cost centre, and SCIM’s core Group schema has no description.

Nothing writes back to Keycloak or Entra ID. SCIM runs one way. So an admin cannot set status; to cut access at once, suspend the person. Suspension also disables their Keycloak account, stopping new sign-ins and session renewal. The portal record is written first, so if the provider is unreachable, access stays denied. Lifting a suspension re-enables the account only if the provider still says active — it is not a way to reactivate someone the provider disabled.

Directory mode

Each organisation chooses a directory mode once, at creation.

ModeWho owns the directory
PORTAL (default)The portal owns who exists, their groups and their department
IDPAn external provider owns them; the directory arrives over SCIMSCIM provisioningCấp tài khoản qua SCIMThe standard your identity provider uses to create, update and remove portal accounts automatically, so people who leave lose access.Open in glossary and the portal refuses to write it. AI feature permissions stay with the portal
In IDP modeWrites
Refused (409 directory_frozen)Create user, set a user’s department, create group, add or remove a member, bulk assign from a group, issue or resend an invitation
Open in both modesDepartment lifecycle (create, rename, archive, set default), what a group grants, roles and capabilities, visibility rules, blocked models, policies, budgets, suspension, which groups a department bills and its priority, withdrawing an open invitation, the pending list

The refusal is a conflict (409), not a permission error, because the organisation’s state decides it — even an Org Owner is refused. Withdrawing an invitation stays open because a grant that should not exist must always be revocable.

Only “set a user’s department” is frozen, so each mode has exactly one writer of a person’s department. Archiving a department with members still works, because the archive sweep moves people directly.

Departments from groups

In IDP mode you cannot move people by hand, so you attach a department to a list of provider groups instead. One Sales department is often Sales-VN + Sales-KH + a leads group. One group can bill at most one department.

Saving a selection replaces the whole list, so nobody is billed twice half-way. An empty list detaches. In PORTAL mode this is refused (409 directory_portal_owned), because the next resolution would undo an admin’s move.

A selection is blocked when it would put someone in the groups of two active departments; the refusal says how many people. Unbilled groups and archived departments bill nobody. Overlap inside one selection is fine — someone in both Sales-VN and Sales-Leads is billed to Sales either way.

Resolution

Resolution runs only in IDP mode. Among the departments billing a group the person is in:

  1. The lowest priority number wins; ties go to department id. Any one of a department’s groups matching is enough.
  2. An archived department never receives anybody.
  3. A person in no billed group falls back to General.

It writes only when the result changes, and runs when a group is provisioned, updated or deleted, when a user is provisioned, and when a selection changes — not when you edit a priority. If a detached organisation has no attached departments left, members return to the default.

When the provider deletes a billed group, the attachment goes with it; the department keeps its name, budget, cost centre and audit history. Once its last group is gone, members move to General on the next resolution. Listings show which groups each department bills.

Why it works this way

Why read the department from the directory, not the token?

A token lives until it expires. Reading the record on every request means a reassignment takes effect on the very next request, without forcing anyone to sign in again.

Why doesn't the portal write back to Entra ID?

A return channel would need directory write permissions across the whole tenant, and an HR sync would overwrite it anyway. SCIM stays one way; suspension is the portal’s own lever.

Why doesn't a deleted upstream group archive its department?

The department was created by an admin and holds a budget and history. Refusing the delete would make the provider retry forever, so the portal lets the link go and keeps the department.

Under the hood

Source-group routes and refusals
RouteRefuses with
PUT /departments/:id/source-groups409 directory_portal_owned, source_group_taken, source_group_overlaps; 404 unknown group
PATCH /departments/{id}A negative priority

Each attachment is a row in its own table, released by its foreign key when the group is deleted upstream, under a unique index so one group bills at most one department. The body carries portal group ids. Name and description are edited through PATCH /departments/{id}.

Where the directory guard lives

The directory_frozen guard sits in the use cases, so the CLI harness hits it without HTTP. SCIM writes through its own repositories and needs no exception. Resolution is checked per call, uses no cache, and its SCIM triggers log a failure without rethrowing — a 500 on an accepted write would make the provider retry forever.

AI Portal Docs
EN VI