Unified.to
All articles

How to Build Org Charts and Employee Directories with Unified's HR & Directory API


February 2, 2026

Org charts and employee directories seem simple—until you support more than one HR system.

Every HRIS models organizational structure differently. Some rely on manager relationships. Others emphasize departments or teams. Locations may be first-class objects, loosely attached metadata, or missing entirely. Even when the data exists, it's often incomplete, inconsistent, or encoded with vendor-specific assumptions.

For a PM or platform engineer, this creates immediate questions:

  • Is reporting hierarchy defined by managers, groups, or both?
  • Are departments hierarchical or flat?
  • Do locations represent offices, regions, or payroll entities?
  • How do we keep directories current without re-syncing everything?

Many products solve this by hardcoding logic per HRIS or forcing a single org model onto all customers. That works early, but breaks as soon as customers expect accuracy across systems.

Unified's HR & Directory API takes a different approach. It does not invent organizational structure. Instead, it exposes explicit, normalized relationship primitives—employees, manager links, groups, locations, and company scope—so you can assemble org charts and directories in a way that matches your product's rules, not a vendor's quirks.

This guide shows how to build org charts and employee directories using that model: fetching employees, resolving reporting hierarchy, composing group and location dimensions, and keeping everything fresh with incremental updates—without vendor-specific connectors or fragile assumptions.

The mental model: org structure is not one thing

Before touching code, it's important to separate three independent dimensions that HR systems often blur together:

  1. Reporting hierarchy
    Who reports to whom.
  2. Organizational grouping
    Teams, departments, divisions, cost centers.
  3. Physical or geographic placement
    Offices, regions, headquarters.

Unified keeps these dimensions distinct. Your org chart or directory is built by composing them, not by assuming one implies the others.

Objects you'll use

Employees (HrisEmployee)

Employees are the anchor for all org views.

Relevant fields:

  • id
  • name, first_name, last_name
  • title
  • manager_id
  • employment_status
  • hired_at, terminated_at
  • groups[]
  • locations[]
  • company_id
  • image_url (expires after one hour)

Important constraints:

  • manager_id may be missing.
  • Some employees belong to multiple groups or locations.
  • Some HRISs do not enforce strict hierarchy.

Groups (HrisGroup)

Groups represent organizational units such as teams or departments.

Relevant fields:

  • id
  • name
  • parent_id
  • type
  • is_active
  • company_id

Groups form a hierarchical graph via parent_id. Their meaning is customer-defined. You should not hardcode 'department' semantics based on type.

Employee → group membership should be treated as authoritative. Group → user backreferences may be incomplete and should not be assumed.

Locations (HrisLocation)

Locations represent physical or geographic placement.

Relevant fields:

  • id
  • name
  • parent_id
  • address (city, region, country)
  • timezone
  • is_active
  • is_hq
  • company_id

Some HRISs model location hierarchies (campus → building). Others do not. Treat parent_id as optional.

Companies (HrisCompany)

Companies provide scoping for multi-entity organizations.

Relevant fields:

  • id
  • name
  • legal_name
  • address

Employees, groups, and locations all reference company_id. This allows you to build org charts per entity without assuming a single global org.

Step 1: Fetch employees incrementally

Employee lists are paginated and support incremental refresh.

import { UnifiedTo } from '@unified-api/typescript-sdk';

const sdk = new UnifiedTo({
  security: { jwt: process.env.UNIFIED_API_KEY! },
});

async function fetchEmployees(connectionId: string) {
  const out = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const page = await sdk.hris.listHrisEmployees({
      connectionId,
      limit,
      offset,
      sort: 'updated_at',
      order: 'asc',
      fields: [
        'id',
        'first_name',
        'last_name',
        'title',
        'manager_id',
        'employment_status',
        'terminated_at',
        'groups',
        'locations',
        'company_id',
        'image_url',
      ].join(','),
    });

    if (!page || page.length === 0) break;

    out.push(...page);
    offset += limit;
  }

  return out;
}

Use updated_gte to refresh only changed records after the initial load.

Step 2: Filter active employees

Directories and org charts typically exclude terminated staff.

function filterActive(employees: any[]) {
  return employees.filter(
    (e) => e.employment_status === 'ACTIVE' && !e.terminated_at
  );
}

Whether to include recent terminations is a product decision, not a data guarantee.

Step 3: Build the reporting hierarchy

Org charts are built from manager_id references.

function buildManagerTree(employees: any[]) {
  const byId = new Map<string, any>();
  const roots: any[] = [];

  for (const e of employees) {
    if (e.id) byId.set(e.id, { ...e, reports: [] });
  }

  for (const e of byId.values()) {
    if (e.manager_id && byId.has(e.manager_id)) {
      byId.get(e.manager_id).reports.push(e);
    } else {
      roots.push(e);
    }
  }

  return roots;
}

Important guardrails:

  • Employees without a valid manager become roots.
  • Circular references may exist in some HRISs. Your UI should tolerate them.
  • Do not assume a single root node.

Step 4: Fetch and index groups

Groups define organizational units independent of reporting lines.

async function fetchGroups(connectionId: string) {
  return await sdk.hris.listHrisGroups({
    connectionId,
    limit: 100,
    offset: 0,
    fields: 'id,name,parent_id,type,is_active,company_id',
  });
}

Build a group tree using parent_id if present. Do not assume group membership implies hierarchy.

Step 5: Fetch and index locations

Locations enable directory views by geography.

async function fetchLocations(connectionId: string) {
  return await sdk.hris.listHrisLocations({
    connectionId,
    limit: 100,
    offset: 0,
    fields: 'id,name,parent_id,address,is_active,is_hq,company_id,timezone',
  });
}

Use location address fields for filters like city, region, or country. Expect missing or partial data.

Step 6: Assemble directory views

At this point, you can construct directories by composing dimensions:

  • By team or department
    Filter employees by groups[].id
  • By location
    Filter employees by locations[].id or address fields
  • By company
    Filter employees by company_id
  • By manager
    Use the reporting tree

Because all list endpoints support query, you can layer search on top without vendor-specific logic.

Step 7: Keep directories fresh

Unified supports incremental refresh across all objects.

Typical pattern:

  1. Initial full load
  2. Store updated_at watermark
  3. Periodically re-fetch with updated_gte
  4. Update local indexes

This avoids full resyncs and works even without webhooks.

What Unified does not assume (by design)

Unified does not:

  • Enforce a single org hierarchy
  • Infer departments from titles
  • Normalize incomplete HR data
  • Guarantee manager relationships exist

It exposes structure as it exists, with consistent shapes and relationships, so your product can apply its own rules transparently.

Closing thoughts

Org charts and employee directories aren't a visualization problem. They're a data modeling problem.

Unified's HR & Directory API gives you the primitives to model organizational structure correctly across HRISs: explicit manager links, hierarchical groups, locations, and company scope—retrieved through a consistent, incremental, and vendor-agnostic interface.

Once those semantics are stable, building org charts and directories becomes straightforward—and scalable across every HR system your customers use.

Start your 30-day free trial

Book a demo

All articles