Skip to content

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 typeSection
employeeEmployee
contractorContractor
vacancyVacancy
projectProject
teamTeam
assignmentAssignment
functional_groupFunctional group
functional_positionFunctional position
functional_assignmentFunctional assignment

Shared shapes: Team allocations, Project allocations, Salary adjustments, Rate adjustments, Job role, Custom attributes, Deletions.

Conventions

TopicRule
externalIdRequired on every record: a non-empty string.
DatesSend YYYY-MM-DD. A full ISO 8601 timestamp is accepted. Allocations, adjustments and functional records keep only its UTC date.
Moneyrate, salary, salaryMin, salaryMax, estimatedCost and bonus accept a number or a numeric string.
Other numbersfte, requiredFte and priority must be numbers. A string is ignored.
Omitted fieldsKeep their current value. null counts as omitted unless a field says otherwise. Allocations and assignments are the exception: see their sections.
Unknown fieldsIgnored.
Send on createMarks a field with no useful default. Send it whenever the record might be new.

Matching

Entity typeA record updatesIf nothing matches
employee, contractor, vacancy, project, teamThe 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.
assignmentThe allocation of the same employee to the same project starting on the same day.An allocation is created.
functional_group, functional_positionThe area or seat with the same externalId in the organisation.One is created.
functional_assignmentThe latest assignment of the same person to the same seat.One is created.

Records aren't matched on name or email.

Employee

FieldTypeNotes
firstNamestringSend on create.
lastNamestringSend on create.
emailstringSend on create. Unique in the organisation: a record with another employee's email fails.
internalEmployeeIdstringYour own identifier, such as a payroll number.
startDatedateDefaults to today on create.
endDatedateLast day of employment.
jobRolestring or objectSee Job role.
teamAllocationsarraySee Team allocations.
projectAllocationsarraySee Project allocations.
salaryAdjustmentsarraySee Salary adjustments.
customAttributesobjectSee Custom attributes.
deletedAtdatePermanently 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

FieldTypeNotes
namestringSend on create. Person or company name.
emailstring
contractorTypestringindividual or company. Defaults to individual.
rateTypestringhourly, daily, monthly or annually.
ratenumber or stringRate in currencyCode.
currencyCodestringISO 4217, such as GBP.
startDatedateContract start.
endDatedateContract end.
jobRolestring or objectSee Job role.
teamAllocationsarraySee Team allocations.
projectAllocationsarraySee Project allocations.
rateAdjustmentsarraySee Rate adjustments.
customAttributesobjectSee Custom attributes.
deletedAtdatePermanently 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

FieldTypeNotes
rolestringSend on create. The title being hired for.
descriptionstring
statusstringopen, committed, filled or cancelled, in any case. Defaults to open. Any other value is ignored and logged as a warning.
ftenumberDefaults to 1.
targetStartDatedateIntended start date.
targetEndDatedateLast day of a fixed-term role.
salarynumber or stringAnnual salary in currencyCode.
salaryMin, salaryMaxnumber or stringDeprecated: use salary. Used only when salary is absent: both bounds store their midpoint, and one bound stores that value.
currencyCodestringISO 4217.
jobRolestring or objectSee Job role.
filledByobject or null{ externalId } of the employee or contractor in the seat. null clears it. See Filled-by reference.
filledByExternalIdstringSame as filledBy.externalId.
filledByEmployeeId, filledByContractorIdstringFlowstate ID of the filler.
teamAllocationsarraySee Team allocations.
projectAllocationsarraySee Project allocations.
customAttributesobjectSee Custom attributes.
deletedAtdatePermanently 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

SendEffect
filledBy: { externalId } or filledByExternalIdLinks the employee or contractor with that externalId.
filledByEmployeeId or filledByContractorIdLinks by Flowstate ID.
filledBy: nullClears the link.
None of theseLeaves the link as it is.
  • Send at most one of these on a record. Combining them fails the record.
  • An externalId matches people this integration synced first, then anyone in the organisation with that externalId.
  • 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

FieldTypeNotes
namestringSend on create.
descriptionstring
projectCodestringYour project code.
startDatedateDefaults to today on create.
endDatedate
estimatedCostnumber or string
prioritynumberDefaults to 0.
customAttributesobjectSee Custom attributes.
deletedAtdatePermanently 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

FieldTypeNotes
namestringSend on create.
descriptionstring
teamTypestringFree text, such as engineering.
parentTeamIdstringThe parent team's externalId. Alias: parentExternalId. See Parent team.
parentTeamNamestringThe parent team's name. See Parent team.
teamManagerEmailstringSee Team manager.
customAttributesobjectSee Custom attributes.
deletedAtdatePermanently 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:

  1. A team this integration synced with externalId equal to parentTeamId.
  2. Any team in the organisation with that externalId.
  3. A team named exactly parentTeamName.
  4. A new team named parentTeamName, with parentTeamId as its externalId.

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.

ValueEffect
OmittedManager unchanged
Matches a userThat user is the manager
Matches no userManager cleared; the record still syncs
Empty stringManager cleared

Assignment

Allocates an employee to a project.

FieldTypeNotes
employeeSourceIdstringRequired. The externalId of an employee this integration synced.
projectSourceIdstringRequired. The externalId of a project this integration synced.
ftenumberDefaults to 1, including on an existing allocation.
startDatedateDefaults to today.
endDatedateOmitted 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.
  • deletedAt and customAttributes aren't supported.
  • To manage an employee's project allocations with removals, send projectAllocations on the employee instead.
  • PUSH hooks with entity type assignment receive more than this: every employee, contractor and vacancy allocation to a team or project. See ctx.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.

FieldTypeNotes
namestringRequired on create. Up to 255 characters.
groupTypestringFree text, such as division. Up to 100 characters.
descriptionstringUp to 10,000 characters.
startDatedateFirst day. Defaults to today on create.
endDatedateLast day. Omit for open-ended. A sync can't clear an end date.
parentExternalIdstringThe parent area's externalId. See Area parentage.
customAttributesobjectSee Custom attributes.
deletedAtdateEnds 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 parentExternalId on an existing area schedules a move from the record's startDate, or from today if it has none. The earlier parent is kept in the history.
  • Omitting parentExternalId leaves 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.

FieldTypeNotes
namestringRequired on create. Up to 255 characters.
groupExternalIdstringRequired on create. The externalId of the seat's area.
requiredFtenumberMore than 0, up to 1, with up to four decimal places. Defaults to 1 on create.
startDatedateFirst day. Defaults to today on create.
endDatedateLast day. Omit for open-ended. A sync can't clear an end date.
reportsToExternalIdstringThe externalId of the seat this one reports to, in the same area.
jobRolestring or objectSee Job role. Omitting it leaves the role unchanged.
descriptionstringUp to 10,000 characters.
customAttributesobjectSee Custom attributes.
deletedAtdateEnds 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 reportsToExternalId seat, hasn't been synced is skipped with a warning. Send areas, then managers' seats, then their reports.
  • Changing reportsToExternalId schedules a reporting-line move from the record's startDate, or from today. Omitting it leaves the line unchanged.
  • A groupExternalId naming 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.

FieldTypeNotes
positionExternalIdstringRequired. The seat's externalId.
employeeExternalIdstringOne of the three. The employee's externalId.
contractorExternalIdstringOne of the three. The contractor's externalId.
vacancyExternalIdstringOne of the three. The vacancy's externalId.
ftenumberMore 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.
startDatedateFirst day in the seat. Defaults to today on create.
endDatedateLast day in the seat. Omit for open-ended. A sync can't clear an end date.
deletedAtdateEnds 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 externalId labels 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. An externalId shared 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 typedeletedAt: '2026-06-30'
functional_groupThe area's last day is 30 June 2026.
functional_positionThe seat's last day is 30 June 2026.
functional_assignmentThe 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 update webhook and PUSH event, never a delete.

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.

FieldTypeNotes
externalIdstringYour ID for the allocation. Recommended: it keeps matching when the team, dates or FTE change. Alias: allocationExternalId.
teamIdstringThe team's externalId. Alias: externalTeamId.
teamNamestringThe team's name. Used when teamId matches nothing.
startDatedateAlias: fromDate. Defaults to the record's startDate in the same payload (targetStartDate for a vacancy), then to today.
endDatedateAlias: toDate. Defaults to the record's endDate in the same payload (targetEndDate for a vacancy). Otherwise open-ended.
ftenumberDefaults to 1.
deletedAtdateAlias: 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.

teamAllocationsEffect
OmittedExisting allocations are unchanged.
An arrayAllocations 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:

FieldTypeNotes
projectIdstringThe project's externalId. Alias: externalProjectId.
projectNamestringThe 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.

FieldTypeNotes
externalIdstringYour ID for the adjustment. Alias: adjustmentExternalId.
effectiveDatedateRequired. The date the salary applies from.
salarynumber or stringRequired. Annual amount.
currencyCodestringRequired. ISO 4217.
bonusnumber or stringReplaced on every update. Omit it to clear.
reasonstringSuch as promotion. Replaced on every update. Omit it to clear.
deletedAtdateDeletes the matched adjustment. Needs externalId or effectiveDate.
  • An entry updates the adjustment with the same externalId from this integration. Without an externalId, it updates the employee's adjustment on the same effectiveDate.
  • An entry missing effectiveDate, salary or currencyCode is 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.

FieldTypeNotes
externalIdstringYour ID for the adjustment. Alias: adjustmentExternalId.
effectiveDatedateRequired. The date the rate applies from.
rateTypestringRequired. hourly, daily, monthly or annually.
ratenumber or stringRequired. Amount in currencyCode.
currencyCodestringRequired. ISO 4217.
reasonstringSuch as renewal. Replaced on every update. Omit it to clear.
deletedAtdateDeletes 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 }.

FieldTypeNotes
titlestringThe role's name.
externalIdstringYour ID for the role.

Send at least one. The role is found in this order:

  1. A role in the organisation with this externalId.
  2. A role named exactly title. If it has no externalId and you sent one, it's stored on the role.
  3. 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 typeSendExample
StringA string'ENG-001'
NumberA number42
DateA YYYY-MM-DD string'2026-06-15'
Date range{ start, end }{ start: '2026-01-01', end: '2026-12-31' }
SelectAn 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 typedeletedAt on the record
employee, contractor, vacancy, project, teamPermanently 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_assignmentEnds the record on that date. See Retiring functional records.
assignmentNot 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.

Flowstate Documentation