| title | useCompensationForm |
|---|---|
| order | 3 |
Creates or updates a compensation row on a job — FLSA classification, pay rate, payment unit, effective date, optional minimum-wage adjustment. Pairs with useJobForm: jobs and their compensations are separate entities in the Gusto API, and this hook focuses exclusively on the compensation side.
import { useCompensationForm, SDKFormProvider } from '@gusto/embedded-react-sdk'Looking for
jobTitle,hireDate,twoPercentShareholder,stateWcCovered/stateWcClassCode? Those moved touseJobForm. Compensation now models only whatPOST /v1/jobs/:jobId/compensationsandPUT /v1/compensations/:idaccept.
Composing with
useJobForm? See Working with Jobs and Compensations for end-to-end patterns covering onboarding stub-fill (POST job → PUT auto-created stub) and steady-state edits.
useCompensationForm accepts a single options object:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
employeeId |
string |
No | — | The UUID of the employee. Drives data fetching for derived helpers (jobs list, work address, minimum wages). Optional for composed flows. |
jobId |
string |
No | — | The UUID of the parent job. Required in create mode (scopes POST /v1/jobs/:jobId/compensations). Optional in update mode — the parent job is derived from the loaded compensation. Can also be passed at submit time when the job is just-created. |
compensationId |
string |
No | — | When present → update mode (PUT /v1/compensations/:id). When absent → create mode (POST /v1/jobs/:jobId/compensations). |
optionalFieldsToRequire |
CompensationOptionalFieldsToRequire |
No | — | Override fields that are optional in a given mode to be required. See Configurable Required Fields. |
defaultValues |
Partial<CompensationFormData> |
No | — | Pre-fill form values. Server data takes precedence on update. |
validationMode |
'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all' |
No | 'onSubmit' |
Passed through to react-hook-form. |
shouldFocusError |
boolean |
No | true |
Auto-focus the first invalid field on submit. Set to false when using composeSubmitHandler. |
withEffectiveDateField |
boolean |
No | true |
When false, hides Fields.EffectiveDate and drops effectiveDate from schema validation. Supply the value via CompensationSubmitOptions.effectiveDate at submit time (e.g. from the parent job's hireDate during onboarding). |
| Field | Rule | Required on create | Required on update | Configurable? |
|---|---|---|---|---|
flsaStatus |
'create' |
Yes | No | Yes (on update) |
paymentUnit |
'create' |
Yes | No | Yes (on update) |
rate |
'create' |
Yes | No | Yes (on update) |
effectiveDate |
'create' |
Yes | No | Yes (on update) |
title |
'never' |
No | No | Yes (either mode) |
adjustForMinimumWage |
(always) | Yes | Yes | No |
minimumWageId |
predicate | When the toggle is on | When the toggle is on | No |
type CompensationOptionalFieldsToRequire = {
create?: Array<'title'>
update?: Array<'title' | 'flsaStatus' | 'paymentUnit' | 'rate' | 'effectiveDate'>
}title is intentionally optional in both modes because you'll typically thread it through useJobForm.Fields.Title (where it's required on create). It remains here as an optional convenience when you're building a single-form steady-state edit screen.
minimumWageId is automatically required when adjustForMinimumWage is true regardless of optionalFieldsToRequire.
The shape of defaultValues:
interface CompensationFormData {
title: string
flsaStatus?: FlsaStatusType // 'Exempt' | 'Salaried Nonexempt' | 'Nonexempt' | 'Owner' | 'Commission Only Exempt' | 'Commission Only Nonexempt'
rate: number
paymentUnit: PaymentUnit // 'Hour' | 'Week' | 'Month' | 'Year' | 'Paycheck'
effectiveDate: string | null // ISO date string (YYYY-MM-DD) or null
adjustForMinimumWage: boolean
minimumWageId: string
}When the hook is given a compensationId (update mode) or its parent job has a current compensation, flsaStatus is seeded from that row. In create mode without a parent compensation, the hook falls back to the employee's primary job's current FLSA status (so adding a secondary job stays consistent with the primary by default), then to defaultValues.flsaStatus. If none of those are available the field renders empty — preselect a value by passing defaultValues.flsaStatus. Requiredness is enforced on submit per the table above.
The hook auto-routes between create and update based on compensationId (and submit options):
| Hook config / submit options | Mode | API call |
|---|---|---|
{ jobId, compensationId } |
update | PUT /v1/compensations/:compensationId (with version) |
{ jobId } (no compensationId) |
create | POST /v1/jobs/:jobId/compensations |
{ employeeId } + submit { jobId, compensationId, compensationVersion } |
update | PUT /v1/compensations/:compensationId (with the supplied version) |
{ employeeId } + submit { jobId } (no compensationId) |
create | POST /v1/jobs/:options.jobId/compensations |
Use the submit-options form for the onboarding stub-fill chain: after useJobForm.actions.onSubmit() creates a job, capture the auto-created compensation's UUID and version from the response, and pass them as { jobId, compensationId, compensationVersion } to this hook's onSubmit to PUT the stub.
The hook returns a discriminated union on isLoading.
{
isLoading: true
errorHandling: HookErrorHandling
}{
isLoading: false
data: {
compensation: Compensation | null // the loaded comp; null in create mode
currentJob: Job | null // the parent job; resolved from compensationId in update mode, or jobId in create mode
minimumWages: MinimumWage[]
minimumEffectiveDate: string | null // typically the parent job's hireDate
maximumEffectiveDate: string | null // the next future-dated comp's effective date, when one exists
hasPendingFutureCompensation: boolean
}
status: {
isPending: boolean
mode: 'create' | 'update'
willDeleteSecondaryJobs: boolean // see "Derived helpers" below
}
actions: {
onSubmit: (
options?: CompensationSubmitOptions,
) => Promise<HookSubmitResult<Compensation> | undefined>
}
errorHandling: HookErrorHandling
form: {
Fields: CompensationFormFields
fieldsMetadata: CompensationFieldsMetadata
hookFormInternals: { formMethods: UseFormReturn }
getFormSubmissionValues: () => CompensationFormOutputs | undefined
}
}interface CompensationSubmitOptions {
/** Override jobId — required when creating a compensation if not configured at hook construction (e.g. when the parent job was just created in the same submit chain). */
jobId?: string
/** Override compensationId — when present, forces update (PUT) routing regardless of hook construction. */
compensationId?: string
/**
* Compensation version for optimistic locking on PUT. Required when forcing
* update routing post-create (e.g. updating the auto-created stub returned
* from POST /v1/employees/:id/jobs). When omitted, the hook reads the
* version from its cached `currentCompensation`.
*/
compensationVersion?: string
/**
* Supply `effectiveDate` at submit time. When `withEffectiveDateField`
* is `true`, this overrides the form's value. When `withEffectiveDateField`
* is `false`, this is the only way to put `effective_date` on the wire —
* the form value is not read in that mode (matching the options-only
* convention of `useWorkAddressForm` / `useHomeAddressForm` / `useJobForm`).
*/
effectiveDate?: string
}onSubmit resolves to a HookSubmitResult<Compensation> containing both the mode ('create' | 'update') and the saved Compensation entity — read the result directly rather than wiring step callbacks.
The hook exposes derived values for driving UX. Static, entity-derived values live under data.*; reactive values that flip with form input live under status.*.
status.willDeleteSecondaryJobs— reactive:truewhen the form is currently positioned to delete the employee's secondary jobs server-side (the "carve-out" branch). Conditions: update mode, the loaded compensation isNonexempt, the form'sflsaStatushas been changed to a non-Nonexemptvalue, and the employee has at least one secondary job. While this flag istruethe hook also locks theeffectiveDatefield — it forces the form value to today and exposesfieldsMetadata.effectiveDate.isDisabled = truesoFields.EffectiveDaterenders as disabled. RevertingflsaStatusback toNonexemptrestores the prioreffectiveDate. Use the flag to render an inline warning ("Saving will delete this employee's secondary jobs"); choose either to render the disabledFields.EffectiveDate(so users can see why the date is forced) or to hide it entirely while the flag is on.data.minimumEffectiveDate— lower bound for theeffectiveDatefield. Typically the parent job'shireDate. Pass this asminto the date picker.data.maximumEffectiveDate— upper bound for theeffectiveDatefield, when a future-dated compensation already exists for this job. Pass this asmaxto the date picker so users can't push a new entry past a pending one.data.hasPendingFutureCompensation—truewhen at least one future-dated compensation exists for this job. Use this to render an explanatory note ("A future rate change is already scheduled for …").
const CompensationErrorCodes = {
REQUIRED: 'REQUIRED',
RATE_MINIMUM: 'RATE_MINIMUM',
RATE_EXEMPT_THRESHOLD: 'RATE_EXEMPT_THRESHOLD',
PAYMENT_UNIT_OWNER: 'PAYMENT_UNIT_OWNER',
PAYMENT_UNIT_COMMISSION: 'PAYMENT_UNIT_COMMISSION',
RATE_COMMISSION_ZERO: 'RATE_COMMISSION_ZERO',
EFFECTIVE_DATE_BEFORE_HIRE: 'EFFECTIVE_DATE_BEFORE_HIRE',
} as constText input for the title tied to this compensation. Use it when the title change should take effect on this compensation's effectiveDate — for example, a future-dated promotion that bundles a new title with a raise.
Bind title via useJobForm.Fields.Title instead when you're creating a job (title is required by the API on job creation) or renaming the active role immediately. Don't render both on the same screen.
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
validationMessages |
{ REQUIRED: string } |
No |
FieldComponent |
ComponentType<TextInputProps> |
No |
Optional in both modes unless optionalFieldsToRequire requires it.
Select dropdown for the employee's FLSA classification (Fair Labor Standards Act status).
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
validationMessages |
{ REQUIRED: string } |
No |
getOptionLabel |
(status: FlsaStatusType) => string |
No |
FieldComponent |
ComponentType<SelectProps> |
No |
Options: Exempt, Salaried Nonexempt, Nonexempt, Owner, Commission Only Exempt, Commission Only Nonexempt.
Conditional availability: This field is undefined when the FLSA status cannot be changed — specifically, when the employee has a non-primary job with a non-Nonexempt status that was already set.
{
Fields.FlsaStatus && (
<Fields.FlsaStatus
label="Employee type"
validationMessages={{ REQUIRED: 'Employee classification is required' }}
/>
)
}Number input for the compensation amount. Formatted as currency.
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
validationMessages |
{ REQUIRED: string, RATE_MINIMUM: string, RATE_EXEMPT_THRESHOLD: string } |
No |
FieldComponent |
ComponentType<NumberInputProps> |
No |
| Code | When it triggers |
|---|---|
REQUIRED |
Rate is empty for non-commission FLSA statuses |
RATE_MINIMUM |
Rate is less than $1.00 |
RATE_EXEMPT_THRESHOLD |
FLSA Exempt employees must meet the federal salary threshold (annualized rate check) |
This field is automatically disabled when the FLSA status is Commission Only (rate is forced to 0).
<Fields.Rate
label="Compensation amount"
validationMessages={{
REQUIRED: 'Amount is a required field',
RATE_MINIMUM: 'Amount must be at least $1.00',
RATE_EXEMPT_THRESHOLD: 'FLSA Exempt employees must meet salary threshold of $35,568/year',
}}
/>Select dropdown for the pay period unit.
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
validationMessages |
{ REQUIRED: string, PAYMENT_UNIT_OWNER: string, PAYMENT_UNIT_COMMISSION: string } |
No |
getOptionLabel |
(unit: PaymentUnit) => string |
No |
FieldComponent |
ComponentType<SelectProps> |
No |
Options: Hour, Week, Month, Year, Paycheck.
This field is automatically disabled when the FLSA status is Owner (forced to Paycheck) or Commission Only (forced to Year).
Date picker for when the new compensation row takes effect.
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
validationMessages |
{ REQUIRED: string, EFFECTIVE_DATE_BEFORE_HIRE: string } |
No |
FieldComponent |
ComponentType<DatePickerProps> |
No |
Required on create. Optional on update (the API keeps the existing effective date when omitted) unless optionalFieldsToRequire.update includes 'effectiveDate'.
Use data.minimumEffectiveDate and data.maximumEffectiveDate to constrain the picker.
This field is automatically disabled (and the form value forced to today) while status.willDeleteSecondaryJobs is true — see Derived helpers. You can render the disabled field as-is, or hide it altogether and key off the flag for a separate inline message.
Conditional availability: This field is undefined when withEffectiveDateField: false. In this mode the hook is strictly options-only — effective_date is omitted from the request body unless you supply CompensationSubmitOptions.effectiveDate at submit time. The willDeleteSecondaryJobs carve-out's UI side effects (force form value to today, disable the field) are inert here because there is no field to render; pass the date through submit options if you need to pin one during the carve-out.
{
Fields.EffectiveDate && (
<Fields.EffectiveDate
label="Effective date"
validationMessages={{
REQUIRED: 'Effective date is required',
EFFECTIVE_DATE_BEFORE_HIRE: 'Effective date cannot be before the hire date',
}}
/>
)
}Checkbox to enable minimum wage adjustment.
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
FieldComponent |
ComponentType<CheckboxProps> |
No |
Conditional availability: This field is undefined when:
- FLSA status is not
Nonexempt - No minimum wages are available for the employee's work location
- The employee's work state does not support tip credits
Select dropdown to choose which minimum wage to adjust to. Only appears when AdjustForMinimumWage is checked.
| Prop | Type | Required |
|---|---|---|
label |
string |
Yes |
description |
ReactNode |
No |
validationMessages |
{ REQUIRED: string } |
No |
FieldComponent |
ComponentType<SelectProps> |
No |
Options: Dynamically populated from minimum wages available at the employee's work location.
import {
useCompensationForm,
SDKFormProvider,
type UseCompensationFormReady,
} from '@gusto/embedded-react-sdk'
function CompensationEditPage({
employeeId,
jobId,
compensationId,
}: {
employeeId: string
jobId: string
compensationId: string
}) {
const compensation = useCompensationForm({ employeeId, jobId, compensationId })
if (compensation.isLoading) return <div>Loading...</div>
return <CompensationFormReady compensation={compensation} />
}
function CompensationFormReady({ compensation }: { compensation: UseCompensationFormReady }) {
const { Fields } = compensation.form
const { hasPendingFutureCompensation, maximumEffectiveDate } = compensation.data
const { willDeleteSecondaryJobs } = compensation.status
return (
<SDKFormProvider formHookResult={compensation}>
<form
onSubmit={async e => {
e.preventDefault()
await compensation.actions.onSubmit()
}}
>
{willDeleteSecondaryJobs && (
<p role="alert">Saving will delete this employee's secondary jobs.</p>
)}
{hasPendingFutureCompensation && (
<p>A future rate change is already scheduled for {maximumEffectiveDate}.</p>
)}
{Fields.FlsaStatus && (
<Fields.FlsaStatus
label="Employee type"
validationMessages={{ REQUIRED: 'Employee classification is required' }}
/>
)}
<Fields.Rate
label="Compensation amount"
validationMessages={{
REQUIRED: 'Amount is required',
RATE_MINIMUM: 'Amount must be at least $1.00',
RATE_EXEMPT_THRESHOLD: 'Exempt employees must meet the salary threshold',
}}
/>
<Fields.PaymentUnit
label="Per"
validationMessages={{
REQUIRED: 'Payment unit is required',
PAYMENT_UNIT_OWNER: 'Owners must be paid per paycheck',
PAYMENT_UNIT_COMMISSION: 'Commission-only employees must be paid annually',
}}
/>
{Fields.EffectiveDate && (
<Fields.EffectiveDate
label="Effective date"
validationMessages={{
REQUIRED: 'Effective date is required',
EFFECTIVE_DATE_BEFORE_HIRE: 'Effective date cannot be before the hire date',
}}
/>
)}
{Fields.AdjustForMinimumWage && (
<Fields.AdjustForMinimumWage label="Adjust for minimum wage" />
)}
{Fields.MinimumWageId && (
<Fields.MinimumWageId
label="Minimum wage"
validationMessages={{ REQUIRED: 'Please select a minimum wage' }}
/>
)}
<button type="submit" disabled={compensation.status.isPending}>
Save
</button>
</form>
</SDKFormProvider>
)
}For the onboarding stub-fill chain (POST job → PUT auto-created stub) and other multi-form flows, see Working with Jobs and Compensations.
- useJobForm — pair this with
useCompensationFormfor full job + compensation editing. - Working with Jobs and Compensations — onboarding stub-fill and steady-state edit recipes.
- Composing Multiple Hooks — coordinate
useJobForm+useCompensationForm(and others) on a single screen.