- Parse. The file is read client-side. Headers are trimmed,
lowercased, and internal whitespace is replaced with underscores, so
External Agent IDandexternal_agent_idare the same column. Empty lines are skipped. - Preview. Each row is classified as
+ INSERT,~ UPDATE,= no-op, or⚠ ERROR, with the specific fields that would change. - Commit. Rows marked INSERT and UPDATE are sent to the control plane one at a time. No-op and error rows are skipped.
Column reference and required fields
These are the columns the import template lays out, in order. A column you leave out of the header row is simply not imported.kind, external_agent_id and display_name are the three columns the
parser checks for before it will accept the file at all. If any of them
is absent from the header row, the upload is rejected with
CSV missing required columns: … and no preview is produced. Fix the
header and re-drop the file.
The Download template action emits exactly these columns plus one
sample row, which is the fastest way to get the header spelling and the
code/email conventions right.
Human vs LLM columns
kind decides which half of the sheet matters:
humanrows use the identity and org columns:email,staff_id,position,employee_type,site_code,hotline_code,tl_email,tm_email.llmrows additionally requireagent_config_id, and may carrydefault_provider,default_modelandhourly_cost_paise.
agent_config_id,
default_provider, default_model, hourly_cost_paise) are only sent
when the row’s kind is llm. Putting a model name on a human row
has no effect on commit.
How rows are matched to existing agents
external_agent_id is the match key, and the only match key. The screen
loads your existing roster and indexes it by external_agent_id, then
for each CSV row:
The comparison is exact on the string. It is not matched on email, name,
or staff id — two rows with the same person’s email but different
external_agent_id values produce two separate agents.
This makes the import idempotent with respect to the key: re-uploading
the same file after a successful commit yields all = no-op rows.
Changing a cell and re-uploading yields an ~ UPDATE for just that
agent, listing just that field.
How references resolve (site and hotline codes, TL/TM emails)
The CSV carries human-readable references, not internal ids. The screen resolves them against your org before the preview renders.
So
hq, HQ and Hq all resolve to the same site, and
Lead@Example.com resolves to the team leader stored as
lead@example.com.
A reference that does not resolve is a row error, not a silent null.
That is deliberate — a typo’d site code would otherwise import an agent
with no site and no indication that anything went wrong.
Team leaders and managers must already have an email on record to be
targetable by tl_email / tm_email. Records without an email are not
in the lookup index and cannot be matched by that column.
Because references resolve against live data, creating the missing site,
hotline or team leader in the admin screens and coming back re-resolves
the rows you already loaded — the preview recomputes when the lookup
data updates, without you re-picking the file.
Reading the diff preview
Once a file parses, the drop zone is replaced by the preview table, with counters across the top for Insert, Update, No change and Error. Each row shows:- Diff — the classification chip described above.
- Kind —
humanorllm, as parsed from the row. - Agent —
display_nameon top,external_agent_idbeneath it. Missing values render as no name / no id. - Email — the parsed email, or
—. - Changes / errors — for an UPDATE, one chip per field that would
change; hovering a chip shows
field: before → after. Only the first five chips are rendered, with a+N morecounter after them. For an ERROR row, this cell lists the validation messages instead.
= no-op
row shows —.
The header button reads Commit N rows, where N is inserts plus
updates. If that number is zero, the button is disabled — there is
nothing to write.
What commit does, and what happens when a row fails
Commit asks for confirmation first. The dialog summarises the work:N INSERT · N UPDATE · N no-change. If there are error rows, the dialog
says so instead and offers to import the remainder, skipping the error
rows.
Then, for each INSERT or UPDATE row in file order, one request at a
time:
- INSERT rows call
adminAgents.createwithorgId,kind,external_agent_id,display_name, and a patch carrying every other parsed column. - UPDATE rows call
adminAgents.updatewithorgId, the matchedagentId, and a patch of the identity and org fields — plus the LLM fields when the row’skindisllm.
done / total, plus a failed count once anything has failed — alongside
a progress bar. The Commit button is disabled for the duration.
When a row’s mutation throws, that row flips to ⚠ ERROR in the table
and the server’s message is appended to its Changes / errors cell as
Import failed: …. That is where to look for the reason a specific
agent did not import; the closing toast is only a pointer
(N of M rows failed — see the Changes / errors column.). On a clean
run you get Imported N rows instead.
After the run the roster list is refreshed, so the agents you just
created or changed appear immediately elsewhere in the admin screens.
Recovering from a partial commit is safe: fix the offending cells in
your CSV and re-upload the whole file. Rows that already landed come
back as = no-op or as an ~ UPDATE limited to the fields you edited,
because matching is by external_agent_id.
Discard clears the file, the parsed rows and the progress counters.
It does not undo anything already committed.
Validation errors and how to fix them
Error rows never reach the control plane. They are excluded from the
commit set, so the counters at the top of the preview tell you exactly
how much of your file will be written.
Related
- Authentication — the API key and scopes the admin procedures authenticate with