Managing employees
Keep your enterprise's members in step with your HR system by uploading a CSV
file. Kesk reads each file as soon as the upload finishes, applies it, and
writes a JSON report of what it did to /from_kesk/reports/.
Kesk runs nothing on a schedule of its own. Every change starts with a file you upload, so how often your roster syncs is up to you: hourly, nightly or weekly.
If you have not connected yet, start with the setup steps.
How it works
Which folder you upload to decides what the file means:
| Folder | Effect |
|---|---|
/to_kesk/employees/add/ | Invite everyone in the file |
/to_kesk/employees/remove/ | Remove everyone in the file |
/to_kesk/employees/ | Treat the file as the complete roster: invite who is new, remove who is missing |
A file is processed when:
- an upload closes with a name ending in
.csv, or - a file is renamed to a name ending in
.csv.
The extension is case-sensitive. roster.CSV or roster.txt is stored and
never processed. Files under /to_kesk/ outside these three folders are
stored and ignored too.
Renaming is a trigger so that a client can upload under a temporary name and rename when the upload is complete. That keeps a half-written file from being read.
Processing is asynchronous. A successful upload means the file was stored, not that it was valid. The outcome is in the .
Uploaded files are not deleted after processing. Uploading a file with the same name overwrites it and processes it again. Deleting a file changes nothing on your roster.
Adding employees
Upload to /to_kesk/employees/add/.
Each row creates a pending invitation for that email address, and Kesk emails it to the person straight away. The person joins the enterprise by accepting it. An invitation expires 30 days after it is sent.
- The invitation stores the row's
first_name,last_nameandexternal_id. - It carries the
rolenamed on the row, which the person receives on accepting. - A row with no
rolecreates an invitation that names no role. - If the same email appears twice in one file, only its first row is used.
- The invitation email is in English and is sent in the name of the account that created the enterprise.
add/ does not compare the file against your roster. A row for someone who
already has a pending invitation, or who is already a member, creates a new
invitation and sends another email. Uploading the same add/ file twice
invites everyone in it twice
(backend#905).
Removing employees
Upload to /to_kesk/employees/remove/.
For each email in the file:
- a pending invitation to that address is revoked, and
- an active member whose primary email matches is removed from the enterprise. Their Kesk account stays; only the membership ends.
An email that matches no one is skipped without an error. A suspended member is not matched, so a suspended member stays in the enterprise.
remove/ has no safety threshold. Every matching person is removed, and no
one is exempt: administrators are removed like anyone else. If a full roster is
uploaded to this folder by mistake, everyone in it loses access in one run.
Check the destination folder in your HR system's delivery settings before the
first scheduled run
(backend#906).
Replacing the roster
Upload to /to_kesk/employees/ itself, not a subfolder.
The file is compared with your current roster, which is every active member with a primary email address plus every pending invitation:
| Person | Result |
|---|---|
In the file, not on the roster | Invited, as in |
On the roster, not in the file | Removed, as in , subject to the |
In both | Left unchanged |
Things this does not do:
- It does not update anyone. For someone already on the roster, a changed
name,
roleorexternal_idin the file is ignored. Change a member's roles withupdateUser. - It protects no one. Anyone active and absent from the file is removed, administrators included. Make sure the people who administer your enterprise are in the file (backend#906).
- It ignores suspended members. They are not on the roster, so leaving them out removes nothing. Listing one sends them a new invitation.
- It never removes a member without a primary email address. Kesk has no address to compare with the file.
Only include people who should be able to spend on the enterprise account.
File requirements
| Requirement | Value |
|---|---|
Encoding | UTF-8 |
Delimiter | Comma |
Quoting | RFC 4180: wrap a field containing a comma or a quote in double quotes |
Header row | Required. The first row names the columns |
Maximum size | 50 MB |
Header names are case-insensitive and may appear in any order. Columns with other names are ignored, and whitespace around every value is trimmed.
| Column | Required | Also accepted as | Notes |
|---|---|---|---|
email | Yes | e-mail, mail | Lowercased. A row whose email is empty or has no @ is skipped and reported |
first_name | No | firstname, given_name | |
last_name | No | lastname, family_name, surname | |
role | No | One role name. It must exactly match the name of an active role in your enterprise, or that row fails | |
external_id | No | employee_id, id | Your identifier for the person, stored on the invitation |
department | No | dept | Accepted, but currently has no effect (backend#905) |
A file without an email column is not processed at all, and produces no
report. That includes a file with no header row, whose first data row is read
as the header. Nothing changes on your roster, and nothing appears in
/from_kesk/reports/ to tell you why
(backend#907). If a report
does not appear after an upload, check the header row first.
Role names are the names an administrator gave your roles. Users and roles shows how to read them off existing members.
Sample CSV
Rows 3 and 5 leave optional values empty. Rows 4 and 6 show quoting, which
is optional unless a value contains a comma or a quote. Enterprise Member and Enterprise Manager are the names of platform-provisioned roles;
use the names your enterprise actually has.
Safety threshold
A full-roster upload to /to_kesk/employees/ is checked before anything is
applied, to catch a truncated or empty export. Let N be the size of your
current roster (active members plus pending invitations) and R the number
of people the file would remove.
| Roster size | The file is rejected when |
|---|---|
More than 5 | R is at least 2 and R × 100 ÷ N, rounded down, is more than 20 |
5 or fewer | R is at least 2 and either the file has no valid rows or R is at least half of N |
A single removal is never rejected. Some examples:
| N | R | Result |
|---|---|---|
10 | 2 | Applied (20%) |
10 | 3 | Rejected (30%) |
101 | 21 | Applied (20.8%, rounded down to 20) |
100 | 21 | Rejected (21%) |
4 | 1 | Applied |
4 | 2 | Rejected (half the roster) |
A rejected file changes nothing: no one is removed and no one is invited.
Kesk writes a rejection_<timestamp>.json report with status
rejected_excessive_churn.
The threshold is fixed. It cannot be raised, lowered or switched off per
enterprise (backend#906). If
a large removal is intended, upload those people to remove/, which is not
checked.
The threshold applies only to /to_kesk/employees/. Neither add/ nor
remove/ is checked.
Reports
Every processed file produces one JSON report in /from_kesk/reports/:
| Upload folder | Report name |
|---|---|
/to_kesk/employees/ | roster_sync_YYYYMMDD_HHMMSS.json |
/to_kesk/employees/ (rejected) | rejection_YYYYMMDD_HHMMSS.json |
/to_kesk/employees/add/ | roster_add_YYYYMMDD_HHMMSS.json |
/to_kesk/employees/remove/ | roster_remove_YYYYMMDD_HHMMSS.json |
The timestamp is the UTC time the report was written, to the second. Reports are read-only: they cannot be deleted over SFTP.
Two files of the same kind that finish processing within the same second get the same report name, and the later report replaces the earlier one. Space out uploads to the same folder (backend#907).
| Field | Meaning |
|---|---|
file_name | The processed file's internal storage path, sftp/<enterpriseId>/to_kesk/.... Not the path you see over SFTP (backend#907) |
file_sha256 | SHA-256 of the file as processed. Compare it with your own hash to tie a report to an upload |
status | completed, completed_with_errors or rejected_excessive_churn |
total_rows | Valid rows in the file. Duplicate emails are counted once, except on remove/ |
added_count | Invitations created |
removed_count | Invitations revoked plus members removed |
unchanged_count | Full roster only: people left as they were. On a rejection, the whole roster. 0 for add/ and remove/ |
error_count | Number of problems |
errors | One message per problem |
completed_with_errors means the file was applied but at least one row was not.
The other rows took effect. Messages look like:
Line 4: Empty email addressLine 7: Malformed email 'ada.example.com'Failed to invite '[email protected]': One or more of the roles named are not available for this enterprise.Failed to remove member '[email protected]': <reason>
Line numbers count CSV records, with the header as record 1.
An illustrative roster_sync report — the shape is exact, the values are
examples:
On a rejection, errors holds the rejection message, and error_count also
counts any skipped rows that the message does not list.
Reference
Generated reference — every type, field and argument of the Enterprise GraphQL API, built from the live schema. SFTP itself is not GraphQL and is documented in full in this section.