Appearance
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
customAttributesobject for arbitrary key-value data. See Custom attributes.
Employee
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | Yes | Employee's first name |
lastName | string | Yes | Employee's last name |
email | string | Yes | Work email address |
internalEmployeeId | string | No | Your internal employee identifier (e.g., payroll number) |
startDate | string (date) | No | Employment start date |
endDate | string (date) | No | Employment end date (for terminated employees) |
jobRole | object or string | No | Role the employee holds. See Job role. |
teamAllocations | array | No | See Team allocations |
projectAllocations | array | No | See Project allocations |
salaryAdjustments | array | No | See Salary adjustments |
customAttributes | object | No | See Custom attributes |
deletedAt | string (date) | No | Mark 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
| Field | Type | Required | Description |
|---|---|---|---|
role | string | Yes | Job title or role name |
description | string | No | Role description |
status | string | No | Vacancy status (defaults to open) |
fte | number | No | Full-time equivalent value (defaults to 1.0) |
targetStartDate | string (date) | No | Intended start date for the hire |
targetFillDate | string (date) | No | Target date to fill the vacancy |
salaryMin | number | No | Minimum salary for the role |
salaryMax | number | No | Maximum salary for the role |
currencyCode | string | No | ISO 4217 currency code for salary values (e.g., GBP, USD) |
jobRole | object or string | No | Role the vacancy is hiring for. See Job role. |
filledBy | object | No | Polymorphic filler reference. See Filled-by reference. |
filledByExternalId | string | No | Shorthand for filledBy.externalId. |
teamAllocations | array | No | See Team allocations |
projectAllocations | array | No | See Project allocations |
customAttributes | object | No | See Custom attributes |
deletedAt | string (date) | No | Mark 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
externalIdis matched againstlive_employees.externalIdandlive_contractors.externalIdin the same organization. - Exactly one match → Flowstate sets
filledByLiveEmployeeIdorfilledByLiveContractorIdaccordingly. - 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Contractor or company name |
email | string | No | Contact email address |
contractorType | string | No | individual or company (defaults to individual) |
rateType | string | No | hourly, daily, or monthly |
rate | number | No | Compensation rate |
currencyCode | string | No | ISO 4217 currency code for rate (e.g., GBP, USD) |
startDate | string (date) | No | Contract start date |
endDate | string (date) | No | Contract end date |
rateAdjustments | array | No | See Rate adjustments |
teamAllocations | array | No | See Team allocations |
projectAllocations | array | No | See Project allocations |
customAttributes | object | No | See Custom attributes |
deletedAt | string (date) | No | Mark 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Project name |
description | string | No | Project description |
projectCode | string | No | Internal project code or reference |
startDate | string (date) | No | Project start date |
endDate | string (date) | No | Project end date |
estimatedCost | number | No | Estimated total cost |
priority | number | No | Priority ranking (defaults to 0) |
customAttributes | object | No | See Custom attributes |
deletedAt | string (date) | No | Mark 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.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Team name |
description | string | No | Team description |
teamType | string | No | Free-text type or category (e.g. engineering, cost-centre) |
parentTeamId | string | No | The parent team's externalId. See Parent team. |
parentTeamName | string | No | Parent team name — used to look up or auto-create the parent if parentTeamId is omitted. See Parent team. |
teamManagerEmail | string | No | Email of the team's manager. See Team manager. |
customAttributes | object | No | See Custom attributes |
deletedAt | string (date) | No | Mark 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:
- Match the parent by
(sourceSystem, externalId), then byexternalIdacross systems. - Fall back to matching by
name. - 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.
| Field | Type | Required | Description |
|---|---|---|---|
employeeSourceId | string | Yes | The externalId of the employee |
projectSourceId | string | Yes | The externalId of the project |
fte | number | No | FTE allocation (defaults to 1.0) |
startDate | string (date) | No | Assignment start date |
endDate | string (date) | No | Assignment 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.
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | No | Your 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. |
teamId | string | No* | External system's team identifier. Preferred over teamName for stability. |
teamName | string | No* | Team display name. Used to look up or auto-create the team if teamId is not provided. |
startDate | string (date) | No | Allocation start date (ISO 8601). Defaults to today. |
endDate | string (date) | No | Allocation end date. Omit or set null for open-ended. |
fte | number | No | FTE allocation (0.0–1.0). Defaults to 1.0. |
deletedAt | string (date) | No | Mark 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.
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | No | Your 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. |
projectId | string | No* | External system's project identifier. Preferred over projectName for stability. |
projectName | string | No* | Project display name. Used to look up or auto-create the project if projectId is not provided. |
startDate | string (date) | No | Allocation start date (ISO 8601). Defaults to today. |
endDate | string (date) | No | Allocation end date. Omit or set null for open-ended. |
fte | number | No | FTE allocation (0.0–1.0). Defaults to 1.0. |
deletedAt | string (date) | No | Mark 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.
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | No | Your 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. |
effectiveDate | string (date) | Yes† | Date the new salary takes effect (ISO 8601). |
salary | number | Yes | Annualised salary amount. |
currencyCode | string | Yes | ISO 4217 currency code (e.g., GBP, USD). |
bonus | number | No | Optional annual bonus amount. |
reason | string | No | Free-text reason for the change (e.g., "promotion"). |
deletedAt | string (date) | No | Mark 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.
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | No | Your 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. |
effectiveDate | string (date) | Yes† | Date the new rate takes effect (ISO 8601). |
rateType | string | Yes | One of hourly, daily, or monthly. |
rate | number | Yes | Rate amount in currencyCode units. |
currencyCode | string | Yes | ISO 4217 currency code (e.g., GBP, USD). |
reason | string | No | Free-text reason for the change (e.g., "renewal"). |
deletedAt | string (date) | No | Mark 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 —effectiveDateor 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.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No* | Role name (e.g., 'Senior Engineer'). Used for lookup by name and for auto-creation when no match exists. |
externalId | string | No* | 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:
- Match by
externalId(scoped to your organisation). - Fall back to matching by
title(the role'sname). If the matched role has noexternalIdyet and you supplied one, Flowstate patches it so subsequent syncs deduplicate correctly. - Auto-create a new role using
titleif nothing matches. Supplying onlyexternalIdwith 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 type | Expected value format | Example |
|---|---|---|
| String | String value | 'ENG-001' |
| Number | Numeric value | 42 or 3.14 |
| Date | ISO 8601 date string | '2025-06-15' |
| Date Range | Object with start and end | { start: '2025-01-01', end: '2025-12-31' } |