The agent directory is the roster screen for an org. It reads dim_agent through the adminAgents tRPC router and renders one row per agent, human or LLM. From here you filter the roster, reassign people in bulk, export a CSV, and open a detail page. The New agent wizard writes into the same table. Everything on the screen is scoped to the active org (tenant.activeOrg.id). Every query and mutation takes that orgId explicitly.

The roster: human and LLM agents in one table

TeleQuick keeps both kinds of worker in a single roster row so that reporting, hierarchy, and hotline assignment work identically for a person and for an AI agent. The kind column picks one of two field sets on the same table: Switching kind in the wizard clears the fields that do not apply to the new kind, so an LLM agent never carries a stale staff_id and a human never carries a stale default_model. The header strip above the table shows the counters the list procedure returns alongside the rows: total, active, humans, llms, onboarding, on_leave, suspended. When the roster query is still loading or has failed, the subtitle reads loading… / unavailable rather than a count — a failed fetch must not look like an empty org. LLM rows, the Humans / LLMs counter, and the Human/LLM kind chips are hidden when the showLLM tweak is off. With the tweak off, the list query is pinned to kind: 'human'.

Identity fields and the external agent ID

Two identity fields are required for every agent, both kinds:
  • Display name — what the table, avatar, and every report show.
  • External agent ID — your own identifier for the agent (the dialer or workforce system’s agent id). It renders in monospace under the display name and is one of the fields the search box matches.
Human agents additionally validate email as an email address if present. LLM agents instead require an agent pool (see below). The wizard hides these messages until your first Continue or Create agent, then tracks each field live; if a required identity field is still missing when you press Create agent, the wizard jumps back to the Identity step. Optional human fields on the Identity step: staff_id, position, employee_type. employee_type is one of: The Last login column is not backed by a server field yet and renders — for every row.

Status vocabulary and the Inactive flag

status is the workflow state of the agent. The Status filter also offers Inactive, which is not a status — it is the soft-delete flag. The distinction that matters: Inactive is the row-level soft-delete flag (active = false), while Terminated is an employment status. Selecting Inactive clears the status constraint and filters on the flag instead; the two are never sent together. The wizard can only create an agent as active or onboarding.

Assignments: site, team manager, team leader, hotline

Four nullable foreign keys place an agent in the org hierarchy. The directory resolves three of them to human-readable codes using lookup queries run in parallel on mount: Any unresolved or unset key renders —. All four are optional at creation; you can leave the whole Hierarchy step untouched and fill it in later from the agent’s detail page. Site and hotline lookups expose both a name and a code: the name is the label in pickers, the code is the hint and the column value.

Linking an LLM agent to an agent pool

For kind = 'llm', agent_config_id is required. Rows in agent_config are the agent pools — the AI voice-agent configs the runtime binds to. The wizard loads the org’s pools (agent_config.id, name, ordered by created_at) and presents them as a picker, so you select a pool rather than paste a raw id. Alongside the pool, an LLM agent can carry default_provider, default_model, and hourly_cost_paise. These are optional and are sent as null when blank. Because the roster row points at the config, deleting an LLM agent also unpublishes its config — see Deactivate vs Delete.

Skill assignments

The wizard’s Skills step picks from adminSkills.list and attaches each choice with two integers: Skills are written after the agent row exists: the wizard creates the agent, takes the returned id, then fires one adminAgents.upsertAgentSkill call per skill in parallel. Failures are tolerated per row. If some succeed and some fail you get a warning toast naming the counts, the agent is still created, and you retry the failed ones from the agent’s Skills tab. upsertAgentSkill is an upsert, so retrying is safe.

Filtering, search, and pagination

All filtering happens server-side. The list procedure accepts: Changing any filter or the search text resets page to 1. The response carries rows, total, perPage, and counters; the pager derives the page count from total / perPage. The previous page’s data stays on screen while the next page loads (keepPreviousData), so the table does not blank out between pages. The lookup queries for sites, hotlines, and team leaders are cached and shared by the filter chips and the FK columns. The header shows the timestamp of the last successful fetch and a refresh button that refetches the roster.

Bulk reassignment

Select rows with the checkbox column to reveal the bulk bar. Two of its actions are reassignments, both implemented with adminAgents.bulkUpdate over the selected ids: Both prompt you to pick the new value first; cancelling the picker cancels the mutation. On success the roster is invalidated and refetched, the selection clears, and a toast reports the updated count returned by the procedure.

Deactivate vs Delete

These are not two flavours of the same thing. Choose based on whether you still need the agent’s history to be readable. Deactivate (adminAgents.deactivate) marks the selected agents terminated with today’s end_date and accepts an optional free-text reason (for example resigned, attrited, layoff). The rows stay in dim_agent, so historical reporting joins still resolve the agent’s name and attributes. The success toast reports the deactivated count. This is the action to use when you only want to stop someone taking calls. Delete (adminAgents.delete) drops the rows permanently and cannot be undone. Historical reporting that joins against those agents loses their name and attributes. For LLM agents, deletion also unpublishes the linked LLM configs — the response returns both deleted and configsDeleted, and the toast reports the number of configs unpublished. Both actions confirm first, and Delete’s confirmation explicitly points you at Deactivate as the reversible alternative.

CSV export columns

Two exports are available, both generated in the browser from the rows currently rendered:
  • Export CSV in the filter bar — the current page, written to agents-page-<n>.csv.
  • Export selected in the bulk bar — the selected rows, written to agents-selected.csv.
Because both work from the rendered rows, the export reflects the active filters and the current page, and it contains the denormalised display values rather than raw ids:

Seat caps and what happens at the limit

Roster size is bounded by the org’s entitlement claim limits.max_concurrent_agents. The wizard reads the cap from the entitlement hook and the current roster size from the same counters.total the directory header uses. When both numbers are known and the cap is a positive number, the wizard treats the org as at capacity once the total reaches the cap. In that state:
  • Create agent is disabled and relabelled with the count and the cap.
  • Hovering it explains that the limit is reached and that you need to upgrade the plan.
  • Submitting anyway surfaces an inline error naming the count, the cap, and the upgrade path.
This check is optimistic and client-side only: it does nothing until both the cap and the count have loaded. The gateway enforces the hard limit regardless of what the console shows.