Appearance
Data model
The fields a PULL hook can send in each record's data, by entity type.
PUSH hooks receive the record as Flowstate stores it, which uses different field names. See ctx.event.
| Entity type | Section |
|---|---|
employee | Employee |
contractor | Contractor |
vacancy | Vacancy |
project | Project |
team | Team |
assignment | Assignment |
functional_group | Functional group |
functional_position | Functional position |
functional_assignment | Functional assignment |
Shared shapes: Team allocations, Project allocations, Salary adjustments, Rate adjustments, Job role, Custom attributes, Deletions.
Conventions
| Topic | Rule |
|---|---|
externalId | Required on every record: a non-empty string. |
| Dates | Send YYYY-MM-DD. A full ISO 8601 timestamp is accepted. Allocations, adjustments and functional records keep only its UTC date. |
| Money | rate, salary, salaryMin, salaryMax, estimatedCost and bonus accept a number or a numeric string. |
| Other numbers | fte, requiredFte and priority must be numbers. A string is ignored. |
| Omitted fields | Keep their current value. null counts as omitted unless a field says otherwise. Allocations and assignments are the exception: see their sections. |
| Unknown fields | Ignored. |
| Send on create | Marks a field with no useful default. Send it whenever the record might be new. |
Matching
| Entity type | A record updates | If nothing matches |
|---|---|---|
employee, contractor, vacancy, project, team | The record this integration synced with the same externalId. Failing that, any record in the organisation with that externalId, which this integration then owns. | A record is created. |
assignment | The allocation of the same employee to the same project starting on the same day. | An allocation is created. |
functional_group, functional_position | The area or seat with the same externalId in the organisation. | One is created. |
functional_assignment | The latest assignment of the same person to the same seat. | One is created. |
Records aren't matched on name or email.
Employee
| Field | Type | Notes |
|---|---|---|
firstName | string | Send on create. |
lastName | string | Send on create. |
email | string | Send on create. Unique in the organisation: a record with another employee's email fails. |
internalEmployeeId | string | Your own identifier, such as a payroll number. |
startDate | date | Defaults to today on create. |
endDate | date | Last day of employment. |
jobRole | string or object | See Job role. |
teamAllocations | array | See Team allocations. |
projectAllocations | array | See Project allocations. |
salaryAdjustments | array | See Salary adjustments. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Permanently deletes the employee. See Deletions. |
js
{
externalId: 'E-1001',
data: {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane.smith@example.com',
internalEmployeeId: 'PAY-2024-042',
startDate: '2024-03-15',
jobRole: { title: 'Senior Engineer', externalId: 'ROLE-042' },
teamAllocations: [
{ externalId: 'TA-7421', teamId: 'T-042', teamName: 'Platform', startDate: '2024-03-15', fte: 1 }
],
salaryAdjustments: [
{ externalId: 'SAL-9001', effectiveDate: '2024-03-15', salary: 95000, currencyCode: 'GBP' }
]
}
}Contractor
| Field | Type | Notes |
|---|---|---|
name | string | Send on create. Person or company name. |
email | string | |
contractorType | string | individual or company. Defaults to individual. |
rateType | string | hourly, daily, monthly or annually. |
rate | number or string | Rate in currencyCode. |
currencyCode | string | ISO 4217, such as GBP. |
startDate | date | Contract start. |
endDate | date | Contract end. |
jobRole | string or object | See Job role. |
teamAllocations | array | See Team allocations. |
projectAllocations | array | See Project allocations. |
rateAdjustments | array | See Rate adjustments. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Permanently deletes the contractor. See Deletions. |
js
{
externalId: 'C-050',
data: {
name: 'Acme Consulting Ltd',
email: 'billing@acme-consulting.com',
contractorType: 'company',
rateType: 'daily',
rate: 800,
currencyCode: 'GBP',
startDate: '2025-01-06',
endDate: '2025-12-31',
rateAdjustments: [
{ externalId: 'RATE-1', effectiveDate: '2025-01-06', rateType: 'daily', rate: 750, currencyCode: 'GBP' },
{ externalId: 'RATE-2', effectiveDate: '2025-07-01', rateType: 'daily', rate: 800, currencyCode: 'GBP', reason: 'renewal' }
]
}
}Vacancy
| Field | Type | Notes |
|---|---|---|
role | string | Send on create. The title being hired for. |
description | string | |
status | string | open, committed, filled or cancelled, in any case. Defaults to open. Any other value is ignored and logged as a warning. |
fte | number | Defaults to 1. |
targetStartDate | date | Intended start date. |
targetEndDate | date | Last day of a fixed-term role. |
salary | number or string | Annual salary in currencyCode. |
salaryMin, salaryMax | number or string | Deprecated: use salary. Used only when salary is absent: both bounds store their midpoint, and one bound stores that value. |
currencyCode | string | ISO 4217. |
jobRole | string or object | See Job role. |
filledBy | object or null | { externalId } of the employee or contractor in the seat. null clears it. See Filled-by reference. |
filledByExternalId | string | Same as filledBy.externalId. |
filledByEmployeeId, filledByContractorId | string | Flowstate ID of the filler. |
teamAllocations | array | See Team allocations. |
projectAllocations | array | See Project allocations. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Permanently deletes the vacancy. See Deletions. |
js
{
externalId: 'POS-101',
data: {
role: 'Senior Software Engineer',
status: 'open',
fte: 1,
targetStartDate: '2026-09-01',
salary: 97500,
currencyCode: 'GBP',
jobRole: { title: 'Senior Engineer', externalId: 'ROLE-042' },
teamAllocations: [{ externalId: 'POS-101-T-042', teamId: 'T-042', teamName: 'Platform', fte: 1 }]
}
}Filled-by reference
| Send | Effect |
|---|---|
filledBy: { externalId } or filledByExternalId | Links the employee or contractor with that externalId. |
filledByEmployeeId or filledByContractorId | Links by Flowstate ID. |
filledBy: null | Clears the link. |
| None of these | Leaves the link as it is. |
- Send at most one of these on a record. Combining them fails the record.
- An
externalIdmatches people this integration synced first, then anyone in the organisation with thatexternalId. - If it matches both an employee and a contractor, the current one is used. A record with no end date beats one that has ended. If both are open-ended, the later start date wins. If both have ended, the later end date wins. Ties go to the employee.
- If it matches nobody, the record fails.
- A person can fill one vacancy at a time. Linking them to a second vacancy fails that record.
Lifecycle and worked examples: Sync positions as vacancies.
Project
| Field | Type | Notes |
|---|---|---|
name | string | Send on create. |
description | string | |
projectCode | string | Your project code. |
startDate | date | Defaults to today on create. |
endDate | date | |
estimatedCost | number or string | |
priority | number | Defaults to 0. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Permanently deletes the project. See Deletions. |
js
{
externalId: 'P-ALPHA',
data: {
name: 'Platform Migration',
projectCode: 'PLAT-2026',
startDate: '2026-04-01',
endDate: '2026-12-31',
estimatedCost: 500000,
priority: 1
}
}Team
| Field | Type | Notes |
|---|---|---|
name | string | Send on create. |
description | string | |
teamType | string | Free text, such as engineering. |
parentTeamId | string | The parent team's externalId. Alias: parentExternalId. See Parent team. |
parentTeamName | string | The parent team's name. See Parent team. |
teamManagerEmail | string | See Team manager. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Permanently deletes the team. Its child teams lose their parent. See Deletions. |
A team synced here is the same team that a team allocation with the same teamId refers to, including one an allocation created.
js
{
externalId: 'T-042',
data: {
name: 'Platform',
teamType: 'engineering',
parentTeamId: 'T-001',
parentTeamName: 'Engineering',
teamManagerEmail: 'ada.lovelace@example.com'
}
}Parent team
The parent is found in this order:
- A team this integration synced with
externalIdequal toparentTeamId. - Any team in the organisation with that
externalId. - A team named exactly
parentTeamName. - A new team named
parentTeamName, withparentTeamIdas itsexternalId.
Without parentTeamName, an unmatched parentTeamId leaves the parent unchanged. Omit both fields to leave the parent unchanged. A team that names itself as its parent is ignored.
Team manager
teamManagerEmail is matched, ignoring case, against users in your organisation.
| Value | Effect |
|---|---|
| Omitted | Manager unchanged |
| Matches a user | That user is the manager |
| Matches no user | Manager cleared; the record still syncs |
| Empty string | Manager cleared |
Assignment
Allocates an employee to a project.
| Field | Type | Notes |
|---|---|---|
employeeSourceId | string | Required. The externalId of an employee this integration synced. |
projectSourceId | string | Required. The externalId of a project this integration synced. |
fte | number | Defaults to 1, including on an existing allocation. |
startDate | date | Defaults to today. |
endDate | date | Omitted means open-ended, including on an existing allocation. |
js
{
externalId: 'ASSIGN-001',
data: {
employeeSourceId: 'E-1001',
projectSourceId: 'P-ALPHA',
fte: 0.5,
startDate: '2026-04-01',
endDate: '2026-12-31'
}
}- A record whose employee or project can't be found is skipped.
deletedAtandcustomAttributesaren't supported.- To manage an employee's project allocations with removals, send
projectAllocationson the employee instead. - PUSH hooks with entity type
assignmentreceive more than this: every employee, contractor and vacancy allocation to a team or project. Seectx.event.
Functional group
An area in the functional structure: a division, department, chapter and so on. It's separate from teams, so an HR system usually syncs both.
Nothing in the functional structure is deleted. Areas, seats and assignments end on a date and stay in the history.
| Field | Type | Notes |
|---|---|---|
name | string | Required on create. Up to 255 characters. |
groupType | string | Free text, such as division. Up to 100 characters. |
description | string | Up to 10,000 characters. |
startDate | date | First day. Defaults to today on create. |
endDate | date | Last day. Omit for open-ended. A sync can't clear an end date. |
parentExternalId | string | The parent area's externalId. See Area parentage. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Ends the area on that day. See Retiring functional records. |
externalId can be up to 255 characters. Another area with the same externalId fails the record.
js
{
externalId: 'UNIT-ENG',
data: {
name: 'Engineering',
groupType: 'division',
startDate: '2026-01-01',
parentExternalId: 'UNIT-TECH'
}
}Area parentage
- The parent must already exist. A record naming an unknown parent is skipped with a warning. Send parents before children, or run the hook again.
- Changing
parentExternalIdon an existing area schedules a move from the record'sstartDate, or from today if it has none. The earlier parent is kept in the history. - Omitting
parentExternalIdleaves the parent unchanged. It never moves an area to the top level. - An area that names itself as its parent is ignored, with a warning.
Functional position
A seat in an area: the demand, independent of who fills it. A seat belongs to one area for its whole life.
| Field | Type | Notes |
|---|---|---|
name | string | Required on create. Up to 255 characters. |
groupExternalId | string | Required on create. The externalId of the seat's area. |
requiredFte | number | More than 0, up to 1, with up to four decimal places. Defaults to 1 on create. |
startDate | date | First day. Defaults to today on create. |
endDate | date | Last day. Omit for open-ended. A sync can't clear an end date. |
reportsToExternalId | string | The externalId of the seat this one reports to, in the same area. |
jobRole | string or object | See Job role. Omitting it leaves the role unchanged. |
description | string | Up to 10,000 characters. |
customAttributes | object | See Custom attributes. |
deletedAt | date | Ends the seat on that day. See Retiring functional records. |
js
{
externalId: 'POS-PAY-STAFF',
data: {
name: 'Staff Engineer, Payments',
groupExternalId: 'UNIT-ENG',
requiredFte: 1,
startDate: '2026-01-01',
reportsToExternalId: 'POS-PAY-LEAD',
jobRole: 'Staff Engineer'
}
}- A seat whose area, or whose
reportsToExternalIdseat, hasn't been synced is skipped with a warning. Send areas, then managers' seats, then their reports. - Changing
reportsToExternalIdschedules a reporting-line move from the record'sstartDate, or from today. Omitting it leaves the line unchanged. - A
groupExternalIdnaming a different area from the seat's current one fails the record. End the seat and create a new one in the other area.
Functional assignment
A person's time in a seat: an employee, a contractor, or a vacancy holding the seat open.
| Field | Type | Notes |
|---|---|---|
positionExternalId | string | Required. The seat's externalId. |
employeeExternalId | string | One of the three. The employee's externalId. |
contractorExternalId | string | One of the three. The contractor's externalId. |
vacancyExternalId | string | One of the three. The vacancy's externalId. |
fte | number | More than 0, up to 1, with up to four decimal places. Defaults to 1 whenever omitted, including on an existing assignment, so send it every time. |
startDate | date | First day in the seat. Defaults to today on create. |
endDate | date | Last day in the seat. Omit for open-ended. A sync can't clear an end date. |
deletedAt | date | Ends the assignment on that day. See Retiring functional records. |
js
{
externalId: 'FILL-4412',
data: {
positionExternalId: 'POS-PAY-STAFF',
employeeExternalId: 'E-1001',
fte: 1,
startDate: '2026-02-01'
}
}- The seat and the person identify the assignment. Sending the same pair again updates its FTE and dates. The record's
externalIdlabels it in the log and the preview. - Set exactly one of the three person fields, including on a record with
deletedAt. None, or more than one, fails the record. - People are matched on
externalId: those this integration synced first, then anyone in the organisation. AnexternalIdshared by an employee and a contractor resolves to the current one, as for a vacancy's filler. - A seat or person not yet synced means the record is skipped with a warning.
- The people in a seat can't hold more FTE than the seat requires on any day. An over-fill fails the record.
- To move someone to another seat, end this assignment and send a new record for the new seat.
Retiring functional records
deletedAt sets the end date to that day. Nothing is removed.
| Entity type | deletedAt: '2026-06-30' |
|---|---|
functional_group | The area's last day is 30 June 2026. |
functional_position | The seat's last day is 30 June 2026. |
functional_assignment | The person's last day in the seat is 30 June 2026. |
- A record already ending on that day, or one Flowstate doesn't have, is skipped.
- An area's child areas and occupied seats must end first. Otherwise the record fails and the log says why.
- Ending a record sends an
updatewebhook and PUSH event, never adelete.
Who a functional sync acts as
Functional records go through the same rules as edits in the app: editing rights, date rules, seat capacity, the audit trail, live updates for other viewers, and webhooks.
A sync acts as the user who created the integration. If that person can't edit an area, the hook can't either, and the log names the refused record. Give the integration's creator editing rights on the top of the structure it syncs.
Unchanged records
A functional record whose values already match isn't written again, so it doesn't create an audit entry, live update or webhook. It's counted as skipped. For an unchanged structure, a daily run reports nothing created or updated.
Team allocations
teamAllocations on an employee, contractor or vacancy. Each entry places the record on a team for a period.
| Field | Type | Notes |
|---|---|---|
externalId | string | Your ID for the allocation. Recommended: it keeps matching when the team, dates or FTE change. Alias: allocationExternalId. |
teamId | string | The team's externalId. Alias: externalTeamId. |
teamName | string | The team's name. Used when teamId matches nothing. |
startDate | date | Alias: fromDate. Defaults to the record's startDate in the same payload (targetStartDate for a vacancy), then to today. |
endDate | date | Alias: toDate. Defaults to the record's endDate in the same payload (targetEndDate for a vacancy). Otherwise open-ended. |
fte | number | Defaults to 1. |
deletedAt | date | Alias: removedAt. Deletes the matched allocation. |
Team. Each entry needs teamId or teamName. The team is found in this order: a team this integration synced with that externalId; any team with that externalId; a team named exactly teamName; a new team named teamName, with teamId as its externalId. An entry that resolves to no team is skipped.
Matching. An entry updates the allocation with the same externalId from this integration. Failing that, it updates the allocation to the same team starting on the same day, wherever that allocation came from. That allocation then belongs to this integration.
The array is the complete set.
teamAllocations | Effect |
|---|---|
| Omitted | Existing allocations are unchanged. |
| An array | Allocations from this integration that no entry matches are deleted. |
[] | Every allocation from this integration on the record is deleted. |
Allocations from other sources (the app, the REST API, other integrations) aren't deleted this way.
js
{
externalId: 'E-1001',
data: {
teamAllocations: [
{ externalId: 'TA-7421', teamId: 'T-042', startDate: '2026-01-01', fte: 0.8 },
{ externalId: 'TA-7422', teamId: 'T-099', startDate: '2026-01-01', fte: 0.2 }
]
}
}teamAssignments, with the aliases above, is still accepted for older hooks.
Project allocations
projectAllocations on an employee, contractor or vacancy. The same rules as team allocations, with:
| Field | Type | Notes |
|---|---|---|
projectId | string | The project's externalId. Alias: externalProjectId. |
projectName | string | The project's name. Used when projectId matches nothing. A project created from it starts today. |
externalId, startDate, endDate, fte and deletedAt work as for team allocations. projectAssignments is still accepted for older hooks.
js
{
externalId: 'E-1001',
data: {
projectAllocations: [
{ externalId: 'PA-9001', projectId: 'P-ALPHA', startDate: '2026-04-01', endDate: '2026-12-31', fte: 0.5 }
]
}
}Salary adjustments
salaryAdjustments on an employee. Each entry is a dated salary.
| Field | Type | Notes |
|---|---|---|
externalId | string | Your ID for the adjustment. Alias: adjustmentExternalId. |
effectiveDate | date | Required. The date the salary applies from. |
salary | number or string | Required. Annual amount. |
currencyCode | string | Required. ISO 4217. |
bonus | number or string | Replaced on every update. Omit it to clear. |
reason | string | Such as promotion. Replaced on every update. Omit it to clear. |
deletedAt | date | Deletes the matched adjustment. Needs externalId or effectiveDate. |
- An entry updates the adjustment with the same
externalIdfrom this integration. Without anexternalId, it updates the employee's adjustment on the sameeffectiveDate. - An entry missing
effectiveDate,salaryorcurrencyCodeis skipped, unless it's a deletion. - Adjustments missing from the array aren't deleted. Use
deletedAt.
Rate adjustments
rateAdjustments on a contractor. Each entry is a dated rate.
| Field | Type | Notes |
|---|---|---|
externalId | string | Your ID for the adjustment. Alias: adjustmentExternalId. |
effectiveDate | date | Required. The date the rate applies from. |
rateType | string | Required. hourly, daily, monthly or annually. |
rate | number or string | Required. Amount in currencyCode. |
currencyCode | string | Required. ISO 4217. |
reason | string | Such as renewal. Replaced on every update. Omit it to clear. |
deletedAt | date | Deletes the matched adjustment. Needs externalId or effectiveDate. |
Matching, skipping and deletion work as for salary adjustments.
Job role
jobRole on an employee, contractor, vacancy or functional position. Send an object, or a string as shorthand for { title }.
| Field | Type | Notes |
|---|---|---|
title | string | The role's name. |
externalId | string | Your ID for the role. |
Send at least one. The role is found in this order:
- A role in the organisation with this
externalId. - A role named exactly
title. If it has noexternalIdand you sent one, it's stored on the role. - A new role named
title.
externalId alone with no match sets no role, and the existing role is unchanged. Omitting jobRole leaves the role unchanged.
js
{ jobRole: { title: 'Senior Engineer', externalId: 'ROLE-042' } }
{ jobRole: 'Senior Engineer' }Custom attributes
customAttributes on an employee, contractor, vacancy, project, team, functional group or functional position. Assignments don't support them.
js
{
externalId: 'E-1001',
data: {
customAttributes: {
cost_centre: 'ENG-001',
clearance_level: 'SC',
contract_renewal: '2027-03-01'
}
}
}- Each key is matched to a custom attribute definition in Settings → Resourcing → Custom Attributes, by its attribute key or, failing that, its name. Use the key: renaming an attribute breaks name matches.
- Keys that match no definition are ignored.
- A definition applies only to the entity types it lists. To set an attribute on an area or seat, its definition must include Functional group or Functional position.
| Field type | Send | Example |
|---|---|---|
| String | A string | 'ENG-001' |
| Number | A number | 42 |
| Date | A YYYY-MM-DD string | '2026-06-15' |
| Date range | { start, end } | { start: '2026-01-01', end: '2026-12-31' } |
| Select | An option's key or label | 'high' |
| Currency | { code, amount } | { code: 'GBP', amount: 1200 } |
A value of the wrong shape is stored as no value. A Select value matching no option is stored as sent.
Functional groups and positions differ in two ways:
- Each changed attribute is written like an edit in the app, with its own audit entry, live update and webhook. An attribute that already matches isn't written again. A record whose only change is an attribute counts as updated.
- A refused attribute, such as a required attribute being cleared, is a warning. The area or seat still syncs, and the log names the attribute and the reason.
Deletions
| Entity type | deletedAt on the record |
|---|---|
employee, contractor, vacancy, project, team | Permanently deletes the record with its allocations and adjustments. The date's value isn't used. A record Flowstate doesn't have is ignored. |
functional_group, functional_position, functional_assignment | Ends the record on that date. See Retiring functional records. |
assignment | Not supported. |
deletedAt on an entry in teamAllocations, projectAllocations, salaryAdjustments or rateAdjustments deletes that entry's match. Allocations are also deleted when they're missing from the array. See Team allocations.
To keep history, end records instead of deleting them: send endDate for employees, contractors and projects, and targetEndDate or status: 'cancelled' for vacancies.