> ## Documentation Index
> Fetch the complete documentation index at: https://f4c7a9e2d8b1-docs.tenzo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# UKG Ready ATS

> Feature coverage and implementation details for UKG Ready integration

## Implementation Overview

**Implementation Type:** Kombo-based Integration\
**Note Format:** Plain Text\
**Status:** In Development

UKG Ready integrates through Kombo's unified API, layered with native UKG Ready REST calls via Kombo passthrough. Kombo's unified data alone cannot deliver phone numbers, job custom fields, applicant custom-field writeback, or applicant-visible notes, so those paths call the native `/ta/rest/v2/companies/{company_reference}/...` API directly.

## Configuration

| Property            | Value                         | Description                                                                     |
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------- |
| Provider Enum       | `ukgready`                    | Identifier in ProviderEnum                                                      |
| Note Format         | `PLAIN`                       | Plain-text notes                                                                |
| Disposition Reasons | Not Supported                 | Rejection handled via stage movement                                            |
| Note Targets        | Application only              | Candidate notes not supported                                                   |
| Passthrough URL     | `/passthrough/ukgready/api`   | Kombo passthrough endpoint                                                      |
| Company Reference   | `ukg_ready_company_reference` | **Required** org config; company id or short name used in every native API path |

<Warning>
  Without `ukg_ready_company_reference`, all native paths silently degrade: no phone numbers, job custom fields never reach the enhanced job cache (so the rules schema stays empty even though schema discovery itself reads the cache without native calls), and custom-field/note writebacks raise errors. Configure it before enabling the integration.
</Warning>

## Setup

### Kombo Connection Registration

Create the Kombo connection link **from Tenzo**, so Kombo receives the Tenzo org ID as `End user origin ID`. If the integration is connected directly in Kombo without it, the `integration_created` webhook carries no org ID and Tenzo never auto-creates the `ats_integrations` row. Recovery: reconnect through a Tenzo-generated link, or manually replay the `integration_created` webhook with the Tenzo org ID.

### Company Reference

Native paths are `/ta/rest/v2/companies/{company_reference}/...`. UKG accepts either the numeric company ID or a short-name reference in `!short_name` format (the Kombo sandbox uses `!kombo`). Validate the value with a native GET after configuring — a wrong reference fails with `Company not found`, it is not arbitrary. If the short name does not work for a tenant, use its numeric UKG company ID.

### Customer-Side Field Setup ("Tenzo Active")

The customer creates a UKG Ready **job requisition custom field** and sets it on the jobs Tenzo should process. Sandbox-proven gotcha (verified by live A/B toggle): with **"Display to Applicant"** unchecked, UKG strips both `custom_field_configs` and `custom_fields` from the requisition API response while the job itself remains visible — making a hidden field indistinguishable from a nonexistent one. The checkbox must be enabled for Tenzo to read the field at all. No workaround through the Kombo passthrough endpoint is currently confirmed; customers who do not want the field applicant-visible need Kombo or UKG to confirm whether a privileged internal-requisition endpoint exists.

### Stage IDs

Capture real stage IDs from synced job payloads after connection (prefer job-specific stage IDs). Stage movement through Kombo is the disposition mechanism, so Copper State's routing stages (Application Received, above/below-threshold, Tenzo Recruiter Review) must be mapped before go-live.

## Feature Coverage

<AccordionGroup>
  <Accordion title="Streaming Operations" defaultOpen>
    * Stream Jobs (Inherited)
    * Stream Applications (Inherited)
    * Stream Candidates (Inherited + native phone enrichment in batches of 8)

    Kombo does not return UKG phone numbers, so candidate streaming backfills phones from the native applicant endpoint.
  </Accordion>

  <Accordion title="Application Management">
    * Move to Stage (Inherited)
    * Get Disposition Reasons — returns an empty list
    * Reject Applications (Single & Bulk) — no-op with logging; use stage movement to disposition
    * Create Applications (Inherited, candidate phone-enriched first)
    * Get All Stages (Inherited)
  </Accordion>

  <Accordion title="Job Management">
    * Get Job by ID (Inherited)
    * Get Enhanced Job — Kombo job + native requisition detail (`custom_field_configs` and `custom_fields` mapped to `custom_field_values`)
    * Batch Job Fetch — follows Kombo cursors until the requested limit, then enriches each job through native passthrough with bounded concurrency (8)

    Job custom-field values feed the rules engine (e.g. "Tenzo Active" filtering).
  </Accordion>

  <Accordion title="Candidate Management">
    * Get Candidate by ID (Inherited + phone enrichment)
    * Get Candidate by Remote ID (Inherited + phone enrichment)
    * Find by Details (Inherited)
    * Get Resume (Inherited)
  </Accordion>

  <Accordion title="Notes & Attachments">
    * Add Note to Application — writes to the Job Application `applicant_comment` via native passthrough (GET existing → append → PUT). Kombo's generic note action targets `company_comment`, which recruiters do not read in the customer's workflow. Sandbox-verified for post-interview summaries and SMS/email activity notes.
    * Add Note to Candidate — Not Supported (no-op)
    * Attachment upload — Inherited via Kombo and sandbox-verified, **but UKG lists uploaded application attachments on the candidate document list**, not on the application.
    * Attachment readback — `GET` application attachments is not supported for UKG Ready (Kombo returns `INTEGRATION.ACTION_NOT_AVAILABLE`); candidate attachment fetching is limited to CV/resume-style documents.
  </Accordion>

  <Accordion title="Resume Ingestion">
    Only files uploaded through UKG's dedicated **resume source** come back through Kombo as type `CV` and feed automatic resume ingestion. PDFs added via UKG's Supporting Documents modal surface as attachment type `OTHER` and are not picked up as resumes.
  </Accordion>

  <Accordion title="Custom Fields">
    * Job Custom Fields — **read-only**; schema inferred from `custom_field_values` on jobs already in the enhanced job cache (open and closed, capped at 10,000, memoized per client instance) — no native calls at schema-discovery time. A field is only discoverable once it has a non-empty value on at least one synced job. UKG `INT` strings are normalized to integers during job enrichment; other values are inferred as boolean, integer, or string from their cached value. Writeback is impossible: requisitions are GET-only in the UKG REST API, so the Tenzo job link field stays disabled (`DisabledTenzoJobLinkCustomField`).
    * Applicant Custom Fields — **writable**; index-based array on the applicant record. Powers application score, candidate score, interview link, and completion rate writebacks.
    * Application Custom Fields — UKG job applications have no custom fields; application-level writes route to the underlying applicant (same pattern as Breezy and RecruitCRM).
  </Accordion>
</AccordionGroup>

## Native Enrichment

Kombo's unified payloads for UKG Ready are missing two things Tenzo depends on: job custom fields and candidate phone numbers. Both are backfilled from the native API during sync.

### Job Custom Fields → Rules Engine ("Tenzo Active")

Kombo's unified job payload does not carry UKG custom fields, so during job sync each Kombo job is enhanced with a native requisition fetch (`GET /recruitment/job-requisitions/{remote_id}`, bounded at 8 concurrent). UKG returns field *definitions* and *values* as two separate arrays matched by `index`:

```json theme={null}
{
  "custom_field_configs": [
    { "index": 0, "label": "Tenzo Active", "type": "CHAR" }
  ],
  "custom_fields": [
    { "index": 0, "value": "Yes" }
  ]
}
```

The client joins them by index into `custom_field_values` on `UkgReadyJob`, which lands in the enhanced job cache — the layer the rules engine filters on. That is the full path that makes "Tenzo Active" job filtering work; without the native fetch, the field never reaches rules. Batch fetches follow Kombo cursors until the requested limit, so large requisition counts are not truncated at one page.

Rules-schema discovery is decoupled from this: it reads `custom_field_values` straight from the enhanced job cache (see Custom Fields above) and issues no native or Kombo calls of its own. The native requisition fetch happens only during job sync, when the cache is populated.

### Candidate Phone Numbers

Kombo returns no phone numbers for UKG Ready candidates, so every candidate read path — streaming, get-by-ID, get-by-remote-ID, and the pre-flight of create-application — backfills phones from the native applicant endpoint (`GET /applicants/{remote_id}`) whenever Kombo left `phone_numbers` empty. During candidate streaming this runs in batches of 8. Ordering honors `phones.preferred_phone` (see Implementation Notes for its dual label/number semantics), so the number the candidate prefers is the one dialed first.

## Applicant Custom-Field Writeback

Four post-interview writebacks target applicant custom fields, each driven by its own org-config setting (an applicant field index picked on the ATS integration settings page):

| Writeback         | Config storage key               |
| ----------------- | -------------------------------- |
| Application score | `application_score_custom_field` |
| Candidate score   | `candidate_score_custom_field`   |
| Interview link    | `interview_link_custom_field`    |
| Completion rate   | `completion_rate_custom_field`   |

Configure each to a **distinct** index (see Duplicate Field Indexes below).

The write path is a native read-modify-write:

1. GET the applicant (`/applicants/{id}`)
2. Merge updates into `applicant_custom_fields` by index
3. PUT the applicant back

Requirements proven against the UKG sandbox:

* The PUT payload **must echo the applicant's `account` and `address` blocks** or UKG rejects the request with validation errors.
* `applicant_custom_fields` is index-based (`[{"index": 0, "value": "..."}]`) — the REST API exposes no labels, so the settings UI offers 50 index placeholders ("Applicant Custom Field N").
* Configured field IDs accept several formats: `"3"`, `"custom_field_3"`, `"applicant_custom_fields.3"`, `"applicant_custom_fields[3]"`.
* UKG can return HTTP 200 with a nested error envelope; responses are checked for `errors` / `user_messages` with severity `ERROR` and raised as `AtsApiError`.

## Implementation Notes

### Applicant Fields Are Person-Level

Applicant custom fields belong to the person, not the application. A candidate with two concurrent applications shares one set of score/link/completion fields — **last write wins**. The per-application source of truth is the `applicant_comment` note.

### Non-Atomic Writebacks

Both the custom-field writeback and the note append are GET → merge → PUT sequences that re-send the full array/comment. Concurrent writers from different processes can silently overwrite each other. Within one call's post-processing, writes are sequential, so this only matters cross-process.

### `preferred_phone` Dual Semantics

The native applicant `phones.preferred_phone` field holds either a phone-type label (`"CELL"`, `"HOME"`, ...) or the raw preferred number itself. The client handles both: a raw number is emitted first; a label reorders the typed phone fields.

### Closed Jobs Disappear

UKG Ready only returns publicly visible jobs. Closed jobs vanish from the API entirely (and filled jobs look open until closed), so missing-job warnings are suppressed for disabled positions (expected stale-cache behavior, not an integration error).

### No Job Code

Kombo returns `job_code` as `null` for UKG Ready jobs. Shared naming code falls back to `remote_id` where a requisition/job code is needed.

### Field Type Mapping

UKG custom-field config types map via `UKG_READY_FIELD_MAPPINGS`: `CHAR` → string, `INT` → integer, `BOOL`/`BOOLEAN` → boolean.

### Duplicate Field Indexes

If two writeback configs resolve to the same applicant field index (e.g. `custom_field_3` and `3`), a warning is logged and their writes will overwrite each other. Configure each writeback to a distinct index.

## Related Files

* Implementation: `server/ats/ukg_ready/ukg_ready_ats_client.py`
* Models: `server/ats/ukg_ready/ukg_ready_models.py`
* Config: `UkgReadyConfig` in `server/ats/config/providers.py`
* Field Mappings: `UKG_READY_FIELD_MAPPINGS` in `server/ats/field_type_mapper.py`
* Base Class: `server/ats/base_kombo_ats_client.py`
* Tests: `server/tests/unit/ats/ukg_ready/`

## See Also

* [ATS Coverage Matrix](/internal/ats-coverage)
* [UKG Ready REST API Documentation](https://secure.saashr.com/ta/docs/rest/public/)
