Skip to content

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

FieldTypeDescription
idstringUnique identifier (CUID). Read-only.
namestringDisplay name, e.g. "Cards & Payments".
groupTypestring | nullFree text for the level: "Area", "Stream", "Domain", "Team"
descriptionstring | nullWhat the group is for.
externalIdstring | nullYour own identifier, e.g. a team code "T-1024".
parentFunctionalGroupIdstring | nullThe group it sits under on the As-of day. null = top level.
startDatedateFirst day the group exists (YYYY-MM-DD, inclusive).
endDatedate | nullLast day, inclusive. null = ongoing.

Functional Position

FieldTypeDescription
idstringCUID. Read-only.
functionalGroupIdstringThe group the seat belongs to. Cannot change after creation.
namestringSeat name, e.g. "Staff Engineer".
descriptionstring | nullWhat the seat is for.
externalIdstring | nullYour own identifier.
jobRoleIdstring | nullOne of the organisation's job roles.
requiredFtenumberAbove 0 and at most 1, to four decimal places.
startDate, endDatedate, date | nullThe seat's lifetime. An end date retires the seat.

Functional Assignment

FieldTypeDescription
idstringCUID. Read-only.
functionalPositionIdstringThe seat. Cannot change after creation.
employeeId / contractorId / vacancyIdstring | nullExactly one is set: who holds the seat. Cannot change after creation.
ftenumberThe share of the seat this person takes: above 0, at most 1.
startDate, endDatedate, date | nullThe days they hold it.

Figures

Returned by the tree and summary reads for each group, counted on the As-of day:

FieldDescription
headcountDistinct people holding a seat in the group.
positions, filledPositions, openPositionsSeats open on the day; how many have at least one occupant; how many are short.
requiredFte, filledFteDemand and supply across the group's seats. The FTE gap is the difference.
arrivals30, leavers30Occupancies starting or ending within 30 days of the As-of day.
monthlyCostRun-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.

MethodPathDescription
GET/treeThe whole visible hierarchy on a day, with figures and managers (at most 500 groups)
GET/List groups — paged, searchable, by parent
GET/:idOne group in full
GET/:id/summaryOne group's figures, occupants, schedule, headcount bridge and upcoming moves
GET/:id/positionsThe seats in a group on a day, with occupants
POST/Create a group
PUT/:idUpdate or retire a group
POST/:id/moveMove a group under another parent from a day
POST/positionsCreate a position
PUT/positions/:idUpdate or retire a position
POST/assignmentsFill a position
PUT/assignments/:idChange an assignment's share or dates
POST/structure/moveChange a reporting line, or move an occupant to another seat, from a day
POST/accessGive a person or login editor (manager) or reader access to a group
DELETE/access/:idRevoke a direct grant
GET/groups/:id/custom-attributesA group's custom attribute values
PUT / DELETE/groups/:id/custom-attributes/:definitionIdSet or clear one attribute on a group
GET/positions/:id/custom-attributesA position's custom attribute values
PUT / DELETE/positions/:id/custom-attributes/:definitionIdSet 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.

ToKey needsCreator needs
Read groupsRead all functional groups (functional_groups_view)An active account in the organisation
Create a top-level group, or change any groupEdit all functional groups (functional_groups_edit)Edit all functional groups
Create beneath, change or move a groupfunctional_groups_editEdit all functional groups, or a manager grant on the group or a parent
Grant or revoke accessShare functional groups (functional_groups_share)Share functional groups
Grant or revoke access on a group they managefunctional_groups_editA manager grant on the group or a parent
See monthlyCost and bridge costsView 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-01

Returns 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
ParameterDescription
searchCase-insensitive match on name or description.
parentIdOnly the direct children of this group.
rootsOnlytrue for top-level groups only.
asOfThe day to read.
offset, limitSkip 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/:id

Body: 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
FieldTypeRequiredDescription
parentFunctionalGroupIdstring | nullYesThe new parent, or null for the top level.
effectiveDatedateYesThe 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/:id

Body: 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/:id

Body: 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.0 across all their seats; for a vacancy, the limit is its own fte

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
FieldTypeRequiredDescription
kind"POSITION" | "ASSIGNMENT"YesMove a seat's reporting line, or move a person to another seat.
idstringYesThe position id (POSITION) or the assignment id (ASSIGNMENT).
targetPositionIdstring | nullYesFor POSITION: the seat it now reports to, or null for nobody. For ASSIGNMENT: the destination seat.
effectiveDatedateYesThe 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
FieldTypeRequiredDescription
functionalGroupIdstringYesThe group.
resourceType"USER" | "EMPLOYEE" | "CONTRACTOR"YesA login directly, or a workforce person whose login is resolved by email.
resourceIdstringYesThe user, employee or contractor id.
access"EDITOR" | "READER"YesEDITOR 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/:definitionId

The 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.

Statuserror.codeWhen
400INVALID_INPUTThe 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.
400INVALID_DATESAn end date before a start date.
400INVALID_FTEA stored FTE outside (0, 1].
400EXACTLY_ONE_RESOURCEAn assignment with none, or more than one, of employeeId, contractorId and vacancyId.
400NO_LINKED_USERAn access grant for an employee or contractor with no active Flowstate login.
403FORBIDDENThe key or its creator may not read or change that group. See Permissions.
403PLAN_NOT_EDITABLEThe scenario named by scenarioId is not open for changes.
403FEATURE_DISABLEDThe functional groups module is off for the organisation.
404NOT_FOUNDNo such record on the As-of day, or the caller can't see it.
409OVER_ALLOCATEDThe seat, or the person, would go over capacity on some day.
409OUTSIDE_ACTIVE_DATESA record would fall outside its group's, seat's or person's dates, or a move's effectiveDate is outside the record's dates.
409HIERARCHY_CYCLEA move would place a group under itself or a descendant.
409OVERLAPPING_PARENTSA group would have two parents on the same day.
409DUPLICATE_EXTERNAL_IDAnother group, or another position, already has that externalId.
409FUTURE_CHANGES_EXISTThe endDate would retire a group or seat before a change already scheduled for it.
409POSITION_AREA_IMMUTABLEA position would change group, or report to a position in another group.
409ASSIGNMENT_IDENTITY_IMMUTABLEAn 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.

Flowstate Documentation