Employee custom table rows
Webhook topics and example payloads for rows in employee custom tables.
Custom tables are the repeating tables you define yourself on an employee's profile — certifications, equipment, dependants, and so on. Each row emits its own event, so an integration can stay in step without polling.
The custom table row emits these webhooks:
employee_table_row_create— a row is addedemployee_table_row_update— a row is changedemployee_table_row_destroy— a row is deleted
Subscribe to each one independently. Selecting one does not subscribe you to the other two, and existing endpoints gain none of them until you add them.
Every payload is self-contained: it carries the employee, the table, and every column with its current value, so you do not need a follow-up API call. Values are the current state only — there is no before-and-after comparison.
When they fire
A delivery is sent when the row is saved or deleted through any of these paths:
- the employee's profile
- the Public API (
POST/PATCH/DELETEon an employee's table rows) - an approved employee change request that adds a row — this emits
employee_table_row_create
One event is sent per changed row, so a change request touching three rows sends three deliveries.
When they do not fire
These topics cover custom tables only. Rows in PeopleForce system tables — position, salary, employment history, and compliance tables — never emit them.
Nothing is sent when:
- the save changes no stored value, so an identical update is silent
- the create, update, delete, or approval fails
- a change request is submitted, rejected, or cancelled — only approval emits
- a row is created by a hire form or preboarding form
Employee custom table row created
Action: employee_table_row_create.
{
"action": "employee_table_row_create",
"data": {
"id": 88213,
"employee": {
"id": 133710,
"employee_number": "IT-1029",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com"
},
"table": {
"id": 4471,
"name": "Certifications",
"internal_name": "certifications"
},
"columns": [
{
"id": 90114,
"name": "Level",
"internal_name": "level",
"type": "EmployeeTableColumns::Text",
"value": "Professional"
},
{
"id": 90115,
"name": "Certified on",
"internal_name": "certified_on",
"type": "EmployeeTableColumns::Date",
"value": null
},
{
"id": 90116,
"name": "Score",
"internal_name": "score",
"type": "EmployeeTableColumns::Number",
"value": "87"
},
{
"id": 90117,
"name": "Renewable",
"internal_name": "renewable",
"type": "EmployeeTableColumns::CheckBox",
"value": "1"
},
{
"id": 90118,
"name": "Rating",
"internal_name": "rating",
"type": "EmployeeTableColumns::SingleSelect",
"value": { "id": "3312", "name": "Excellent" }
},
{
"id": 90119,
"name": "Topics",
"internal_name": "topics",
"type": "EmployeeTableColumns::MultipleSelect",
"value": [
{ "id": "3315", "name": "Ruby" },
{ "id": "3316", "name": "Rails" }
]
},
{
"id": 90120,
"name": "Mentor",
"internal_name": "mentor",
"type": "EmployeeTableColumns::Reference",
"value": { "id": "133711", "name": "Grace Hopper" }
},
{
"id": 90121,
"name": "Notes",
"internal_name": "notes",
"type": "EmployeeTableColumns::LongText",
"value": "Renewal due next year"
}
],
"meta": {
"created_at": "2026-08-17T09:41:12.204Z",
"updated_at": "2026-08-17T09:41:12.204Z"
}
}
}Employee custom table row updated
Action: employee_table_row_update. The payload shape matches
employee_table_row_create, with the row's values after the change.
Employee custom table row deleted
Action: employee_table_row_destroy. The payload shape matches
employee_table_row_create, and carries the row's final values as they stood
before deletion.
Field reference
| Field | Type | Notes |
|---|---|---|
data.id | integer | The row ID. |
data.employee | object | Employee identity: id, employee_number, first_name, last_name, and email. |
data.table | object | The custom table: id, name, and internal_name. |
data.columns | array | Every column on the table, in the order shown in the product. Deleted columns are excluded. |
data.columns[].id | integer | The column ID. |
data.columns[].internal_name | string | The stable API name. Prefer it over name, which admins can rename freely. |
data.columns[].type | string | The column type — see the table below. |
data.columns[].value | varies | The current value — see the table below. |
data.meta | object | created_at and updated_at for the row. |
Column values by type
Every column on the table appears in columns, whether or not it holds a value.
An empty column is present with a null value.
type | value shape | Empty |
|---|---|---|
EmployeeTableColumns::Text | string | null |
EmployeeTableColumns::Number | string, e.g. "87" | null |
EmployeeTableColumns::Date | string, e.g. "2026-08-14" | null |
EmployeeTableColumns::CheckBox | string, "1" when ticked or "0" when not | null |
EmployeeTableColumns::LongText | string, HTML stripped to plain text | null |
EmployeeTableColumns::SingleSelect | { "id": "…", "name": "…" } | null |
EmployeeTableColumns::MultipleSelect | array of { "id": "…", "name": "…" } | [] |
EmployeeTableColumns::Reference | { "id": "…", "name": "…" } — an employee | null |
Four details to code against:
- Scalars are JSON strings, not JSON numbers or booleans. A number column
sends
"87", a date sends"2026-08-14", and a checkbox sends"1"or"0". Both write paths store the value as text and the payload returns it uncast, so parse rather than assume. Rows loaded by an older import may still hold a raw JSON number. - An unticked checkbox sends
"0", which is a real value, not an empty one. Only a checkbox that was never set sendsnull. - A multi-select is always an array, never
null. An empty one is[]. - The
idinside a select, multi-select, or reference value is a string, because that is how the row stores it. The surroundingcolumns[].idanddata.idare integers.
A reference value keeps its stored id even when the referenced employee can no
longer be found, in which case name is null.
Testing a payload
The sample-payload endpoint (GET /webhooks?key=<topic>) does not yet cover the
three custom table row topics and returns 204 No Content for them. To see a
real payload, subscribe an endpoint, change a row, then open the delivery in
Settings → Webhooks → delivery history.
