Appearance
Sync positions as vacancies
For PULL hooks with entity type vacancy that sync positions from an HR system. Every field: Data model → Vacancy.
The model
- A vacancy is a budgeted seat. Its team and project allocations say where the budget sits.
- Employees and contractors are separate records. A vacancy is filled by linking one of them.
- A vacancy counts as filled while its filler has no end date, or an end date that hasn't passed. After the filler's last day it counts as open again, without another sync.
statusis separate from the link. Filling a vacancy doesn't change itsstatus, so sendfilledyourself if you track it.
How a vacancy is costed: How costs are calculated.
Filling a vacancy
| 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 |
Sync employees and contractors before the vacancies they fill. Resolution rules and errors: Filled-by reference.
Lifecycle
1. A position is opened
js
{
externalId: 'POS-001',
data: {
role: 'Staff Engineer',
status: 'open',
targetStartDate: '2026-03-01',
salary: 120000,
currencyCode: 'GBP',
teamAllocations: [
{ externalId: 'POS-001-platform', teamId: 'TEAM-platform', fte: 1, startDate: '2026-03-01' }
]
}
}2. Someone fills it
js
{
externalId: 'POS-001',
data: {
role: 'Staff Engineer',
status: 'filled',
teamAllocations: [
{ externalId: 'POS-001-platform', teamId: 'TEAM-platform', fte: 1, startDate: '2026-03-01' }
],
filledByExternalId: 'EMP-ada-001'
}
}Filled vacancies are hidden from the default vacancy list.
3. The seat moves to another team
End the old allocation and add the new one, so the history keeps both:
js
{
externalId: 'POS-001',
data: {
role: 'Staff Engineer',
teamAllocations: [
{ externalId: 'POS-001-platform', teamId: 'TEAM-platform', fte: 1, startDate: '2026-03-01', endDate: '2026-05-31' },
{ externalId: 'POS-001-growth', teamId: 'TEAM-growth', fte: 1, startDate: '2026-06-01' }
],
filledByExternalId: 'EMP-ada-001'
}
}An allocation from this integration that's missing from the array is deleted, not ended.
4. The person leaves and the seat reopens
js
{
externalId: 'POS-001',
data: {
role: 'Staff Engineer',
status: 'open',
filledBy: null
}
}teamAllocations is omitted, so the allocations stay as they are.
5. The position is closed
Keep the history:
js
{ externalId: 'POS-001', data: { status: 'cancelled', targetEndDate: '2026-08-01' } }Or delete the vacancy and its allocations permanently:
js
{ externalId: 'POS-001', data: { deletedAt: '2026-08-01' } }Re-runs and errors
- Sending the same record again leaves the vacancy as it is, though it counts as updated.
teamAllocationsandprojectAllocationsare the complete set of this integration's allocations whenever they're sent. See Team allocations.- Each record is applied on its own. A failed record is logged as
Record <externalId>: <message>and the others still apply. - When allocations change, each page of a vacancy run logs a line of counts in this form:
Page 1 nested allocations — team: +created ~updated -deleted (=unchanged); project: ….
Filling through the REST API
To fill a vacancy on demand rather than by sync, use POST /vacancies/:id/fill. It creates the employee or contractor and fills the vacancy in one request.