Appearance
Functional Groups
Read and change the functional structure: groups (areas, streams, domains, teams), the positions inside them, who fills each position and for how long, and who manages what. The product guide is at Functional groups; this page is the REST reference.
Everything is dated. Reads take an asOf day and return the structure as it stood on that day; writes carry startDate and endDate, and nothing is ever deleted — a record is retired by giving it an end date.
Module switch
Functional groups is a product module. Until it is switched on for the organisation, every endpoint here answers 403 with { "error": { "code": "FEATURE_DISABLED" } }.
Objects
Functional Group
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (CUID). Read-only. |
name | string | Display name, e.g. "Cards & Payments". |
groupType | string | null | Free text for the level: "Area", "Stream", "Domain", "Team"… |
description | string | null | What the group is for. |
externalId | string | null | Your own identifier, e.g. a team code "T-1024". |
parentFunctionalGroupId | string | null | The group it sits under on the As-of day. null = top level. |
startDate | date | First day the group exists (YYYY-MM-DD, inclusive). |
endDate | date | null | Last day, inclusive. null = ongoing. |
Functional Position
| Field | Type | Description |
|---|---|---|
id | string | CUID. Read-only. |
functionalGroupId | string | The group the seat belongs to. Cannot change after creation. |
name | string | Seat name, e.g. "Staff Engineer". |
description | string | null | What the seat is for. |
externalId | string | null | Your own identifier. |
jobRoleId | string | null | One of the organisation's job roles. |
requiredFte | number | Above 0 and at most 1, to four decimal places. |
startDate, endDate | date, date | null | The seat's lifetime. An end date retires the seat. |
Functional Assignment
| Field | Type | Description |
|---|---|---|
id | string | CUID. Read-only. |
functionalPositionId | string | The seat. Cannot change after creation. |
employeeId / contractorId / vacancyId | string | null | Exactly one is set: who holds the seat. Cannot change after creation. |
fte | number | The share of the seat this person takes: above 0, at most 1. |
startDate, endDate | date, date | null | The days they hold it. |
Figures
Returned by the tree and summary reads for each group, counted on the As-of day:
| Field | Description |
|---|---|
headcount | Distinct people holding a seat in the group. |
positions, filledPositions, openPositions | Seats open on the day; how many have at least one occupant; how many are short. |
requiredFte, filledFte | Demand and supply across the group's seats. The FTE gap is the difference. |
arrivals30, leavers30 | Occupancies starting or ending within 30 days of the As-of day. |
monthlyCost | Run-rate for the As-of month in the reporting currency, or null when the caller may not see cost (see who sees cost). |
Endpoints
All paths are under /api/v1/org/:orgId/functional-groups.
| Method | Path | Description |
|---|---|---|
GET | /tree | The whole visible hierarchy on a day, with figures and managers (at most 500 groups) |
GET | / | List groups — paged, searchable, by parent |
GET | /:id | One group in full |
GET | /:id/summary | One group's figures, occupants, schedule, headcount bridge and upcoming moves |
GET | /:id/positions | The seats in a group on a day, with occupants |
POST | / | Create a group |
PUT | /:id | Update or retire a group |
POST | /:id/move | Move a group under another parent from a day |
POST | /positions | Create a position |
PUT | /positions/:id | Update or retire a position |
POST | /assignments | Fill a position |
PUT | /assignments/:id | Change an assignment's share or dates |
POST | /structure/move | Change a reporting line, or move an occupant to another seat, from a day |
POST | /access | Give a person or login editor (manager) or reader access to a group |
DELETE | /access/:id | Revoke a direct grant |
GET | /groups/:id/custom-attributes | A group's custom attribute values |
PUT / DELETE | /groups/:id/custom-attributes/:definitionId | Set or clear one attribute on a group |
GET | /positions/:id/custom-attributes | A position's custom attribute values |
PUT / DELETE | /positions/:id/custom-attributes/:definitionId | Set or clear one attribute on a position |
:id is the record's Flowstate ID. GET /:id also accepts a group's externalId.
Every read accepts asOf=YYYY-MM-DD, which defaults to today in UTC. Reads and writes accept ?scenarioId= to act on a scenario instead of live data. The exception is managers and access: those always change live access.
Writes return { "data": { "id": "…" } }. POST to /, /positions, /assignments and /access returns 201. Every other write returns 200.
Permissions
A key acts with its creator's access, narrowed to the permissions on the key.
| To | Key needs | Creator needs |
|---|---|---|
| Read groups | Read all functional groups (functional_groups_view) | An active account in the organisation |
| Create a top-level group, or change any group | Edit all functional groups (functional_groups_edit) | Edit all functional groups |
| Create beneath, change or move a group | functional_groups_edit | Edit all functional groups, or a manager grant on the group or a parent |
| Grant or revoke access | Share functional groups (functional_groups_share) | Share functional groups |
| Grant or revoke access on a group they manage | functional_groups_edit | A manager grant on the group or a parent |
See monthlyCost and bridge costs | View Financial Summary (financials_view_summary) | See who sees cost |
With scenarioId, the creator also needs access to the scenario. Writes need the scenario to be open for changes.
Read the hierarchy
GET /api/v1/org/:orgId/functional-groups/tree?asOf=2026-12-01Returns every group the caller may see on that day, in name order. Each group has its figures, its managers (marked when inherited from a parent) and its parentFunctionalGroupId. The read stops at 500 groups, so read larger structures a group at a time.
bash
curl "https://{tenant}.flowstate.inc/api/v1/org/{orgId}/functional-groups/tree?asOf=2026-12-01" \
-H "Authorization: Bearer private_..."List groups
GET /api/v1/org/:orgId/functional-groups?search=&parentId=&rootsOnly=&asOf=&offset=0&limit=20| Parameter | Description |
|---|---|
search | Case-insensitive match on name or description. |
parentId | Only the direct children of this group. |
rootsOnly | true for top-level groups only. |
asOf | The day to read. |
offset, limit | Skip offset groups (default 0) and return up to limit (default 20, at most 100). |
The response is { "data": { "groups": [...], "total": 42, "canCreateRoot": true, "canShare": false } }, sorted by name. There is no meta object. Each group adds viewerAccess (EDITOR or READER) and hasChildren.
Read one group
GET /api/v1/org/:orgId/functional-groups/:id?asOf=
GET /api/v1/org/:orgId/functional-groups/:id/summary?asOf=
GET /api/v1/org/:orgId/functional-groups/:id/positions?asOf=GET /:id returns the group with its parent, a page of its positions and their assignments, its access grants and its history. offset and limit page the positions (default 0 and 20, limit at most 100), and totalPositions gives the full count.
GET /:id/positions returns the seats open on the As-of day. It takes search (name or description), parentId (seats reporting to that position), rootsOnly, offset and limit.
The summary contains:
- the group's figures
- arrivals and leavers in the next 30 days, by name
- the schedule: every seat and occupancy overlapping the next twelve months
- the twelve-month headcount bridge: opening, joiners, leavers, closing and cost per month
- the last 20 changes
Create or update a group
POST /api/v1/org/:orgId/functional-groups
PUT /api/v1/org/:orgId/functional-groups/:idBody: a Functional Group without id. name and startDate are required. Unknown fields are rejected.
PUT replaces the whole group. A field you leave out goes back to null, so send every field you want to keep. The exception is groupType, which keeps its value when left out. On PUT, parentFunctionalGroupId is ignored: use Move a group.
Creating a top-level group needs Edit all functional groups. Creating a sub-group needs edit rights on the parent.
bash
curl -X POST "https://{tenant}.flowstate.inc/api/v1/org/{orgId}/functional-groups" \
-H "Authorization: Bearer private_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Card Issuing",
"groupType": "Team",
"externalId": "T-1101",
"parentFunctionalGroupId": "cmtvie6b30015ks6doqh0pqk1",
"startDate": "2026-10-01"
}'To retire a group, PUT it back with an endDate. Retire its positions and sub-groups first.
Move a group
POST /api/v1/org/:orgId/functional-groups/:id/move| Field | Type | Required | Description |
|---|---|---|---|
parentFunctionalGroupId | string | null | Yes | The new parent, or null for the top level. |
effectiveDate | date | Yes | The day the move applies from. Earlier days keep the old parent. |
The group to move is :id in the path. You need edit rights on the group and on the new parent. Moving to the top level needs Edit all functional groups.
effectiveDate must fall inside the group's current placement, or 409 OUTSIDE_ACTIVE_DATES. A group can't be moved under itself or any of its descendants, on any day: that returns 409 HIERARCHY_CYCLE.
Create or update a position
POST /api/v1/org/:orgId/functional-groups/positions
PUT /api/v1/org/:orgId/functional-groups/positions/:idBody: a Functional Position without id. functionalGroupId, name, requiredFte and startDate are required. jobRoleId must name an existing job role.
PUT replaces the whole position. functionalGroupId must match the seat's group, or 409 POSITION_AREA_IMMUTABLE. An endDate retires the seat, and every occupancy must end by then.
Fill a position, or change an assignment
POST /api/v1/org/:orgId/functional-groups/assignments
PUT /api/v1/org/:orgId/functional-groups/assignments/:idBody: a Functional Assignment without id. Send exactly one of employeeId, contractorId or vacancyId. fte and startDate are required.
On PUT, the seat and the person must be unchanged, or 409 ASSIGNMENT_IDENTITY_IMMUTABLE. To move someone, end this assignment and create the next, or use the structure move below.
The write returns 409 OVER_ALLOCATED if, on any day, either:
- the seat's occupants would exceed its
requiredFte - the person would exceed
1.0across all their seats; for a vacancy, the limit is its ownfte
It returns 409 OUTSIDE_ACTIVE_DATES if the assignment falls outside the seat's dates or the person's employment dates.
Change a reporting line, or move an occupant
POST /api/v1/org/:orgId/functional-groups/structure/move| Field | Type | Required | Description |
|---|---|---|---|
kind | "POSITION" | "ASSIGNMENT" | Yes | Move a seat's reporting line, or move a person to another seat. |
id | string | Yes | The position id (POSITION) or the assignment id (ASSIGNMENT). |
targetPositionId | string | null | Yes | For POSITION: the seat it now reports to, or null for nobody. For ASSIGNMENT: the destination seat. |
effectiveDate | date | Yes | The day the change applies from. |
A position can only report to a position in the same group (409 POSITION_AREA_IMMUTABLE), never to itself or one of its own reports. ASSIGNMENT needs a targetPositionId. Both seats, and the assignment, must be active on effectiveDate, or 409 OUTSIDE_ACTIVE_DATES. Moving an occupant ends the old assignment the day before and starts the new one on effectiveDate.
Managers and access
POST /api/v1/org/:orgId/functional-groups/access
DELETE /api/v1/org/:orgId/functional-groups/access/:id| Field | Type | Required | Description |
|---|---|---|---|
functionalGroupId | string | Yes | The group. |
resourceType | "USER" | "EMPLOYEE" | "CONTRACTOR" | Yes | A login directly, or a workforce person whose login is resolved by email. |
resourceId | string | Yes | The user, employee or contractor id. |
access | "EDITOR" | "READER" | Yes | EDITOR makes them a manager: they may change the group and everything beneath it. READER grants read access; the app only appoints managers, so a READER grant can only be made through the API. |
Rights inherit downwards. Granting or revoking needs Share functional groups, or a manager grant on that group. See Permissions. DELETE /access/:id takes the grant id shown on the group, and rights inherited from a parent survive.
Grants always change live access straight away. A scenarioId on these two endpoints is ignored.
Custom attributes
Groups and positions carry custom attribute values, defined under Settings → Resourcing → Custom Attributes for the FUNCTIONAL_GROUP and FUNCTIONAL_POSITION entity types.
GET /api/v1/org/:orgId/functional-groups/groups/:id/custom-attributes
PUT /api/v1/org/:orgId/functional-groups/groups/:id/custom-attributes/:definitionId
DELETE /api/v1/org/:orgId/functional-groups/groups/:id/custom-attributes/:definitionIdThe same three paths exist under /positions/:id/…. The PUT body is the value object described in Custom Attributes; DELETE clears an optional attribute. Both are audited and appear in the group's or position's history.
Errors
Errors come back as { "error": { "code": "...", "message": "..." } }. INVALID_INPUT from body or query validation also carries details, one entry per issue.
| Status | error.code | When |
|---|---|---|
400 | INVALID_INPUT | The body or query fails validation. For example: a missing required field, an unknown field, a date that isn't YYYY-MM-DD, an fte or requiredFte outside (0, 1] or with more than four decimal places, or an assignment without exactly one person. |
400 | INVALID_DATES | An end date before a start date. |
400 | INVALID_FTE | A stored FTE outside (0, 1]. |
400 | EXACTLY_ONE_RESOURCE | An assignment with none, or more than one, of employeeId, contractorId and vacancyId. |
400 | NO_LINKED_USER | An access grant for an employee or contractor with no active Flowstate login. |
403 | FORBIDDEN | The key or its creator may not read or change that group. See Permissions. |
403 | PLAN_NOT_EDITABLE | The scenario named by scenarioId is not open for changes. |
403 | FEATURE_DISABLED | The functional groups module is off for the organisation. |
404 | NOT_FOUND | No such record on the As-of day, or the caller can't see it. |
409 | OVER_ALLOCATED | The seat, or the person, would go over capacity on some day. |
409 | OUTSIDE_ACTIVE_DATES | A record would fall outside its group's, seat's or person's dates, or a move's effectiveDate is outside the record's dates. |
409 | HIERARCHY_CYCLE | A move would place a group under itself or a descendant. |
409 | OVERLAPPING_PARENTS | A group would have two parents on the same day. |
409 | DUPLICATE_EXTERNAL_ID | Another group, or another position, already has that externalId. |
409 | FUTURE_CHANGES_EXIST | The endDate would retire a group or seat before a change already scheduled for it. |
409 | POSITION_AREA_IMMUTABLE | A position would change group, or report to a position in another group. |
409 | ASSIGNMENT_IDENTITY_IMMUTABLE | An assignment update changes its seat or its person. |
Webhooks and hooks
Every write here fires an outbound webhook (functional_group, functional_position, functional_assignment; see Outbound Webhooks) and any matching custom integration PUSH hook. PULL hooks can create and update these records from an external system keyed by externalId; see the custom integrations data model.