Skip to content

Data Model Reference

This page documents the fields available for each entity type in Custom Integrations.

For PULL hooks, these are the fields you include in the data object of each record. For PUSH hooks, these are the fields available on ctx.event.before and ctx.event.after.

Common patterns

  • externalId — Required in PULL records. This is your external system's unique identifier for the record. Flowstate uses it to match records across syncs.
  • Dates — Use ISO 8601 format: YYYY-MM-DD (e.g., 2025-06-15).
  • Custom attributes — All entity types support a customAttributes object for arbitrary key-value data. See Custom attributes.

Employee

FieldTypeRequiredDescription
firstNamestringYesEmployee's first name
lastNamestringYesEmployee's last name
emailstringYesWork email address
internalEmployeeIdstringNoYour internal employee identifier (e.g., payroll number)
startDatestring (date)NoEmployment start date
endDatestring (date)NoEmployment end date (for terminated employees)
jobRoleobject or stringNoRole the employee holds. See Job role.
teamAllocationsarrayNoSee Team allocations
projectAllocationsarrayNoSee Project allocations
salaryAdjustmentsarrayNoSee Salary adjustments
customAttributesobjectNoSee Custom attributes
deletedAtstring (date)NoMark this employee as deleted upstream — Flowstate hard-deletes the matching record. See Deletions and orphan cleanup.
js
{
  externalId: 'emp-001',
  data: {
    firstName: 'Jane',
    lastName: 'Smith',
    email: 'jane.smith@example.com',
    internalEmployeeId: 'EMP-2024-042',
    startDate: '2024-03-15',
    jobRole: { title: 'Senior Engineer', externalId: 'ROLE-042' },
    teamAllocations: [
      { teamId: 'team-042', teamName: 'Platform', startDate: '2024-03-15', fte: 1.0 }
    ]
  }
}

Vacancy

FieldTypeRequiredDescription
rolestringYesJob title or role name
descriptionstringNoRole description
statusstringNoVacancy status (defaults to open)
ftenumberNoFull-time equivalent value (defaults to 1.0)
targetStartDatestring (date)NoIntended start date for the hire
targetFillDatestring (date)NoTarget date to fill the vacancy
salaryMinnumberNoMinimum salary for the role
salaryMaxnumberNoMaximum salary for the role
currencyCodestringNoISO 4217 currency code for salary values (e.g., GBP, USD)
jobRoleobject or stringNoRole the vacancy is hiring for. See Job role.
filledByobjectNoPolymorphic filler reference. See Filled-by reference.
filledByExternalIdstringNoShorthand for filledBy.externalId.
teamAllocationsarrayNoSee Team allocations
projectAllocationsarrayNoSee Project allocations
customAttributesobjectNoSee Custom attributes
deletedAtstring (date)NoMark this vacancy as deleted upstream — Flowstate hard-deletes the matching record. See Deletions and orphan cleanup.

Filled-by reference

When you know the person currently filling a position, send their externalId and Flowstate will resolve it to either an employee or a contractor automatically. You don't need to know which type the person is on Flowstate's side.

js
{
  externalId: 'POS-12345',
  data: {
    role: 'Senior Engineer',
    targetStartDate: '2026-03-01',
    teamAllocations: [{ teamId: 'TEAM-platform', fte: 1.0, startDate: '2026-03-01' }],
    filledBy: { externalId: 'EMP-ada-001' },
    // or, equivalently:
    // filledByExternalId: 'EMP-ada-001'
  }
}

Resolution rules:

  • The externalId is matched against live_employees.externalId and live_contractors.externalId in the same organization.
  • Exactly one match → Flowstate sets filledByLiveEmployeeId or filledByLiveContractorId accordingly.
  • Match on both sides → the record fails with an "ambiguous" error.
  • No match → the record fails with a "not found" error.
  • To clear an existing fill (re-open the slot), send filledBy: null.

A vacancy is treated as currently filled while one of these FKs is set and the filler's endDate is unset or in the future. Once the filler's endDate passes, the slot is open again — the forecast cost resumes automatically, no further sync action required.

See Custom Integrations → Vacancies for a full guide to the position lifecycle and worked examples covering team moves, early terminations, and re-opens.

js
{
  externalId: 'vac-101',
  data: {
    role: 'Senior Software Engineer',
    description: 'Backend systems team',
    status: 'open',
    fte: 1.0,
    targetStartDate: '2025-09-01',
    salaryMin: 85000,
    salaryMax: 110000,
    currencyCode: 'GBP',
    jobRole: { title: 'Senior Engineer', externalId: 'ROLE-042' }
  }
}

Contractor

FieldTypeRequiredDescription
namestringYesContractor or company name
emailstringNoContact email address
contractorTypestringNoindividual or company (defaults to individual)
rateTypestringNohourly, daily, or monthly
ratenumberNoCompensation rate
currencyCodestringNoISO 4217 currency code for rate (e.g., GBP, USD)
startDatestring (date)NoContract start date
endDatestring (date)NoContract end date
rateAdjustmentsarrayNoSee Rate adjustments
teamAllocationsarrayNoSee Team allocations
projectAllocationsarrayNoSee Project allocations
customAttributesobjectNoSee Custom attributes
deletedAtstring (date)NoMark this contractor as deleted upstream — Flowstate hard-deletes the matching record. See Deletions and orphan cleanup.
js
{
  externalId: 'ctr-050',
  data: {
    name: 'Acme Consulting Ltd',
    email: 'billing@acme-consulting.com',
    contractorType: 'company',
    rateType: 'daily',
    rate: 750,
    currencyCode: 'GBP',
    startDate: '2025-01-06',
    endDate: '2025-06-30'
  }
}

Project

FieldTypeRequiredDescription
namestringYesProject name
descriptionstringNoProject description
projectCodestringNoInternal project code or reference
startDatestring (date)NoProject start date
endDatestring (date)NoProject end date
estimatedCostnumberNoEstimated total cost
prioritynumberNoPriority ranking (defaults to 0)
customAttributesobjectNoSee Custom attributes
deletedAtstring (date)NoMark this project as deleted upstream — Flowstate hard-deletes the matching record. See Deletions and orphan cleanup.
js
{
  externalId: 'proj-alpha',
  data: {
    name: 'Platform Migration',
    description: 'Migrate core services to new infrastructure',
    projectCode: 'PLAT-2025',
    startDate: '2025-04-01',
    endDate: '2025-12-31',
    estimatedCost: 500000,
    priority: 1
  }
}

Team

Teams are first-class records with their own externalId. Syncing a team here deduplicates against the same team whether it was created manually, by this hook, or auto-created as a side-effect of an employee/contractor/vacancy team allocation — they all converge on externalId.

FieldTypeRequiredDescription
namestringYesTeam name
descriptionstringNoTeam description
teamTypestringNoFree-text type or category (e.g. engineering, cost-centre)
parentTeamIdstringNoThe parent team's externalId. See Parent team.
parentTeamNamestringNoParent team name — used to look up or auto-create the parent if parentTeamId is omitted. See Parent team.
teamManagerEmailstringNoEmail of the team's manager. See Team manager.
customAttributesobjectNoSee Custom attributes
deletedAtstring (date)NoMark this team as deleted upstream — Flowstate hard-deletes the matching record. Child teams are detached (their parent is cleared), not cascaded. See Deletions and orphan cleanup.
js
{
  externalId: 'team-platform',
  data: {
    name: 'Platform',
    description: 'Core infrastructure & developer platform',
    teamType: 'engineering',
    parentTeamId: 'team-engineering',
    teamManagerEmail: 'ada.lovelace@example.com'
  }
}

Parent team

A team's place in the hierarchy is set with parentTeamId (the parent's externalId, preferred for stability) or parentTeamName. Resolution mirrors a team allocation's team reference:

  1. Match the parent by (sourceSystem, externalId), then by externalId across systems.
  2. Fall back to matching by name.
  3. Auto-create the parent (using parentTeamName) when nothing matches and a name was supplied.

A team can never be its own parent — a self-reference (the parent resolving to the same record) is ignored rather than rejected. Omit both fields to leave an existing parent unchanged.

Team manager

Hooks don't know Flowstate user IDs, so a team references its manager by email. teamManagerEmail is matched case-insensitively against an existing user in your organisation:

  • A match links the manager.
  • An email with no matching user leaves the team unmanaged (the record still syncs — it is not rejected).
  • Omitting the field leaves an existing manager untouched; sending an empty string clears it.

Assignment

Assignments link employees to projects. Both employeeSourceId and projectSourceId reference the externalId values you used when syncing the corresponding employee and project records.

FieldTypeRequiredDescription
employeeSourceIdstringYesThe externalId of the employee
projectSourceIdstringYesThe externalId of the project
ftenumberNoFTE allocation (defaults to 1.0)
startDatestring (date)NoAssignment start date
endDatestring (date)NoAssignment end date
js
{
  externalId: 'assign-001',
  data: {
    employeeSourceId: 'emp-001',
    projectSourceId: 'proj-alpha',
    fte: 0.5,
    startDate: '2025-04-01',
    endDate: '2025-12-31'
  }
}

TIP

Assignments require that both the employee and the project have already been synced via PULL hooks using the same integration. The employeeSourceId and projectSourceId are matched against existing records by their externalId.

Team allocations

Nested inside an Employee, Vacancy, or Contractor record as teamAllocations. Each entry places the parent on a team over a time range. Teams are auto-created when neither teamId nor an existing name matches.

FieldTypeRequiredDescription
externalIdstringNoYour source system's stable identifier for the allocation row itself (not the team). When supplied, Flowstate matches by (integration, externalId) first — survives changes to team, startDate, or fte without creating a duplicate. Strongly recommended when your source can produce one.
teamIdstringNo*External system's team identifier. Preferred over teamName for stability.
teamNamestringNo*Team display name. Used to look up or auto-create the team if teamId is not provided.
startDatestring (date)NoAllocation start date (ISO 8601). Defaults to today.
endDatestring (date)NoAllocation end date. Omit or set null for open-ended.
ftenumberNoFTE allocation (0.0–1.0). Defaults to 1.0.
deletedAtstring (date)NoMark the matched allocation row as removed upstream — Flowstate hard-deletes it. See Deletions and orphan cleanup.

*At least one of teamId or teamName must be provided.

js
{
  externalId: 'emp-001',
  data: {
    firstName: 'Jane',
    lastName: 'Smith',
    email: 'jane.smith@example.com',
    teamAllocations: [
      { externalId: 'alloc-7421', teamId: 'team-042', startDate: '2024-03-15', fte: 0.8 },
      { externalId: 'alloc-7422', teamId: 'team-099', startDate: '2024-03-15', fte: 0.2 }
    ]
  }
}

Matching priority: externalId (exact) → (parent, team, startDate) natural key (day-precision). On a match, the row is updated when any field differs and recorded as Unchanged when byte-identical. On no match, a new row is created.

Legacy aliases

teamAssignments (with externalTeamId, fromDate, toDate, allocationExternalId) is still accepted for backward compatibility with older hooks. New hooks should use the canonical names above.

Project allocations

Nested inside an Employee, Vacancy, or Contractor record as projectAllocations. Each entry places the parent on a project over a time range. Projects are auto-created when neither projectId nor an existing name matches.

FieldTypeRequiredDescription
externalIdstringNoYour source system's stable identifier for the allocation row itself (not the project). When supplied, Flowstate matches by (integration, externalId) first — survives changes to project, startDate, or fte without creating a duplicate. Strongly recommended when your source can produce one.
projectIdstringNo*External system's project identifier. Preferred over projectName for stability.
projectNamestringNo*Project display name. Used to look up or auto-create the project if projectId is not provided.
startDatestring (date)NoAllocation start date (ISO 8601). Defaults to today.
endDatestring (date)NoAllocation end date. Omit or set null for open-ended.
ftenumberNoFTE allocation (0.0–1.0). Defaults to 1.0.
deletedAtstring (date)NoMark the matched allocation row as removed upstream — Flowstate hard-deletes it. See Deletions and orphan cleanup.

*At least one of projectId or projectName must be provided.

js
{
  externalId: 'emp-001',
  data: {
    firstName: 'Jane',
    lastName: 'Smith',
    email: 'jane.smith@example.com',
    projectAllocations: [
      { externalId: 'alloc-9001', projectId: 'proj-alpha', startDate: '2025-04-01', endDate: '2025-12-31', fte: 0.5 }
    ]
  }
}

Matching priority: externalId (exact) → (parent, project, startDate) natural key (day-precision). On a match, the row is updated when any field differs and recorded as Unchanged when byte-identical. On no match, a new row is created.

Legacy aliases

projectAssignments (with externalProjectId, fromDate, toDate, allocationExternalId) is still accepted for backward compatibility with older hooks. New hooks should use the canonical names above.

When to use nested allocations vs the top-level Assignment entity

Use nested teamAllocations / projectAllocations to sync an employee and their allocations in a single record. Use the top-level Assignment entity when employees and projects are synced separately and you want to link them afterwards by externalId.

Salary adjustments

Nested inside an Employee record as salaryAdjustments. Each entry is a dated salary change.

FieldTypeRequiredDescription
externalIdstringNoYour source system's stable identifier for the adjustment row itself. When supplied, Flowstate matches by (integration, externalId) first — survives effectiveDate corrections without creating a duplicate.
effectiveDatestring (date)Yes†Date the new salary takes effect (ISO 8601).
salarynumberYesAnnualised salary amount.
currencyCodestringYesISO 4217 currency code (e.g., GBP, USD).
bonusnumberNoOptional annual bonus amount.
reasonstringNoFree-text reason for the change (e.g., "promotion").
deletedAtstring (date)NoMark the matched adjustment as removed upstream — Flowstate hard-deletes it.

†Required when externalId is not supplied; in that case the natural key (employee, effectiveDate) is the only way to locate the row.

Matching priority: externalId (exact) → (employee, effectiveDate) natural key (day-precision). On match, compares salary/bonus/currency/reason and updates if any field changed; records Unchanged when byte-identical. Entries missing required fields (other than during deletion) are silently skipped.

Rate adjustments

Nested inside a Contractor record as rateAdjustments. Each entry is a dated rate change.

FieldTypeRequiredDescription
externalIdstringNoYour source system's stable identifier for the adjustment row itself. When supplied, Flowstate matches by (integration, externalId) first — survives effectiveDate corrections without creating a duplicate.
effectiveDatestring (date)Yes†Date the new rate takes effect (ISO 8601).
rateTypestringYesOne of hourly, daily, or monthly.
ratenumberYesRate amount in currencyCode units.
currencyCodestringYesISO 4217 currency code (e.g., GBP, USD).
reasonstringNoFree-text reason for the change (e.g., "renewal").
deletedAtstring (date)NoMark the matched adjustment as removed upstream — Flowstate hard-deletes it.

†Required when externalId is not supplied; in that case the natural key (contractor, effectiveDate) is the only way to locate the row.

Matching priority: externalId (exact) → (contractor, effectiveDate) natural key (day-precision). On match, compares rateType/rate/currency/reason and updates if any field changed; records Unchanged when byte-identical. Entries missing required fields (other than during deletion) are silently skipped.

js
{
  externalId: 'ctr-050',
  data: {
    name: 'Acme Consulting Ltd',
    contractorType: 'company',
    rateType: 'daily',
    rate: 800,
    currencyCode: 'GBP',
    rateAdjustments: [
      { effectiveDate: '2025-01-06', rateType: 'daily', rate: 750, currencyCode: 'GBP' },
      { effectiveDate: '2025-07-01', rateType: 'daily', rate: 800, currencyCode: 'GBP', reason: 'renewal' }
    ]
  }
}

Deletions and orphan cleanup

Flowstate honours two complementary signals when a row should disappear.

Per-row deletedAt

Every nested type (teamAllocations, projectAllocations, salaryAdjustments, rateAdjustments) and every top-level record (Employee, Vacancy, Contractor, Project) accepts an optional deletedAt ISO date string. When present:

  • For nested rows, Flowstate locates the matching row by externalId (preferred) or natural key, then hard-deletes it. Other fields on the entry are ignored — effectiveDate or the parent identifier is enough to find the row.
  • For top-level records, Flowstate hard-deletes the matching Live* row. Foreign-key cascades clean up dependent allocations, adjustments, and plan correlations.
js
{
  externalId: 'emp-001',
  data: {
    teamAllocations: [
      { externalId: 'alloc-7421', deletedAt: '2026-04-29' }   // remove just this allocation
    ]
  }
}

{
  externalId: 'emp-001',
  data: { deletedAt: '2026-04-29' }                            // remove the whole employee
}

Implicit orphan cleanup (allocations only)

For teamAllocations and projectAllocations, Flowstate scopes existing rows to the integration's sourceSystem and treats the incoming array as the complete state of allocations originating from that integration. Any pre-existing row tagged with this sourceSystem that doesn't appear in the payload (under any matchable form) is hard-deleted as an orphan. Allocations stamped with a different sourceSystem (manual entry, other integrations) are never touched.

This is gated on the field being present in the payload. Sending teamAllocations: [] will wipe all integration-owned allocations for that parent; omitting the field entirely is a no-op. So customers that emit allocations only conditionally won't accidentally lose data — just be careful with empty arrays.

salaryAdjustments and rateAdjustments do not participate in implicit orphan cleanup; use deletedAt to remove an adjustment row.

Job role

Nested inside an Employee or Vacancy record as jobRole. Accepts either an object with title and/or externalId, or a bare string (shorthand for { title: "…" }). Omit the field to leave the role unchanged on updates.

FieldTypeRequiredDescription
titlestringNo*Role name (e.g., 'Senior Engineer'). Used for lookup by name and for auto-creation when no match exists.
externalIdstringNo*Your external system's identifier for the role. Preferred over title for stability.

*At least one of title or externalId must be provided.

js
{
  externalId: 'emp-001',
  data: {
    firstName: 'Jane',
    lastName: 'Smith',
    email: 'jane@example.com',
    jobRole: { title: 'Senior Engineer', externalId: 'ROLE-042' }
  }
}

Resolution order:

  1. Match by externalId (scoped to your organisation).
  2. Fall back to matching by title (the role's name). If the matched role has no externalId yet and you supplied one, Flowstate patches it so subsequent syncs deduplicate correctly.
  3. Auto-create a new role using title if nothing matches. Supplying only externalId with no match is treated as "no role" — the role is not created.

Shorthand string form (equivalent to { title: 'Senior Engineer' }):

js
{ jobRole: 'Senior Engineer' }

See Configuration: Job Roles for the full role management model.

Custom attributes

All entity types (except assignments) support a customAttributes field for syncing arbitrary data to Flowstate's custom attribute system.

js
{
  externalId: 'emp-001',
  data: {
    firstName: 'Jane',
    lastName: 'Smith',
    email: 'jane@example.com',
    customAttributes: {
      cost_centre: 'ENG-001',
      security_clearance: 'SC',
      contract_renewal: '2026-03-01'
    }
  }
}

Custom attributes are matched by key to Custom Attribute Definitions configured in your Flowstate organisation (Settings → Configuration → Custom Attributes). Keys that don't match a definition are silently ignored.

Supported field types

Definition typeExpected value formatExample
StringString value'ENG-001'
NumberNumeric value42 or 3.14
DateISO 8601 date string'2025-06-15'
Date RangeObject with start and end{ start: '2025-01-01', end: '2025-12-31' }

Flowstate Documentation