Skip to content

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.
  • status is separate from the link. Filling a vacancy doesn't change its status, so send filled yourself if you track it.

How a vacancy is costed: How costs are calculated.

Filling a vacancy

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

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.
  • teamAllocations and projectAllocations are 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.

Flowstate Documentation