# UKG Pro

### Overview

The **UKG Pro WFM** plugin syncs workforce-management data from your [UKG Pro WFM](https://developer.ukg.com/wfm/docs/quick-start-doc) tenant into Snowflake using the Omnata Sync Engine. It is an **inbound-only** plugin — data flows from UKG Pro into Snowflake tables.

***

### Prerequisites

Before creating a connection you will need:

1. A **UKG Pro WFM tenant** with API access enabled.
2. A **service account** (username + password) in UKG Pro with at least read access to the timekeeping, scheduling, and commons data APIs.
3. An **API application** registered in UKG Pro (see the Authentication section below for the values you will need to collect).

For step-by-step instructions on registering an API application and locating the connection parameters, refer to the [UKG Pro WFM API Quick Start guide](https://developer.ukg.com/wfm/docs/quick-start-doc) and the [UKG Developer Portal authentication documentation](https://developer.ukg.com/wfm/docs/authentication).

***

### Authentication

UKG Pro WFM uses OAuth 2.0 with a password-realm grant. You will need to collect the following values before creating the Omnata connection. These come from two places: your UKG tenant configuration and the API application you register in UKG.

| Value                   | Where to find it                                                                                                                                |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **API Host**            | Your tenant hostname — visible in your browser's address bar when logged into UKG Pro (e.g. `mytenant.npr.mykronos.com`)                        |
| **Token URL**           | The base URL of the OAuth token endpoint for your tenant — typically the same origin as the API host (e.g. `https://mytenant.npr.mykronos.com`) |
| **Client ID**           | Generated when you register an API application in UKG Pro                                                                                       |
| **Client Secret**       | Generated alongside the Client ID when you register the API application                                                                         |
| **Audience**            | The API audience value for your tenant, provided during application registration. Typically a URL like `https://wfm.ukg.net/api`                |
| **Realm**               | The authentication realm for your tenant (e.g. `UltiPro`) — found in your UKG tenant settings                                                   |
| **Username / Password** | The service account credentials used to authenticate against the API                                                                            |

{% hint style="info" %}
Access tokens are automatically refreshed by the plugin during long syncs — you do not need to manage token expiry yourself.
{% endhint %}

***

### Connection Setup

Navigate to **Connections → New Connection** and select **UKG Pro WFM**. Fill in the following fields using the values collected above:

| Field             | Description                                          | Example                             |
| ----------------- | ---------------------------------------------------- | ----------------------------------- |
| **API Host**      | Your UKG tenant hostname (without `https://`)        | `mytenant.npr.mykronos.com`         |
| **Token URL**     | Base URL for the OAuth token endpoint                | `https://mytenant.npr.mykronos.com` |
| **Client ID**     | OAuth application client ID                          | `abc123`                            |
| **Client Secret** | OAuth application client secret *(stored encrypted)* | —                                   |
| **Username**      | Service account username                             | `svc_omnata`                        |
| **Password**      | Service account password *(stored encrypted)*        | —                                   |
| **Audience**      | OAuth audience value                                 | `https://mytenant.npr.mykronos.com` |
| **Realm**         | Authentication realm                                 | `UltiPro`                           |

When you save, Omnata will test the connection by verifying that the credentials can authenticate and reach the UKG timekeeping API. A failure here typically means the service account lacks read permissions or one of the credential fields is incorrect.

***

### Sync Configuration

When creating or editing an inbound sync, the following options are available:

| Option                            | Default      | Description                                                                                                                                                         |
| --------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Start date**                    | `2022-01-01` | The earliest date to pull data from on the very first (full-refresh) sync. Ignored for incremental syncs, which use the last-sync timestamp instead.                |
| **Detect deletes via audit logs** | Off          | When enabled *(timecards stream only)*, the plugin queries UKG audit logs for `Delete` events and marks the corresponding timecard records as deleted in Snowflake. |

#### Tuning parameters

| Parameter                                | Default | Description                                                                                                              |
| ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Fetch batch size** (`rest_batch_size`) | `500`   | Number of employee identifiers sent per API batch request. Reduce this if you encounter timeouts or payload-size errors. |

***

### Data Streams

#### `employees`

Employee demographics fetched via the UKG commons data API. All employees returned by the `All Home` hyperfind are included.

**Sync strategy:** Full refresh only.\
**Primary key:** `id` (the UKG employee identifier).

Key columns:

| Column                                | Description                               |
| ------------------------------------- | ----------------------------------------- |
| `id`                                  | Unique employee identifier                |
| `EMP_COMMON_FULL_NAME`                | Full name                                 |
| `EMP_COMMON_PRIMARY_JOB`              | Primary job title                         |
| `EMP_COMMON_PRIMARY_ORG`              | Primary organisation path                 |
| `PEOPLE_EMP_STATUS`                   | Employment status (e.g. Active, Inactive) |
| `PEOPLE_HOME_COST_CENTER`             | Home cost centre                          |
| `PEOPLE_HIRE_DATE`                    | Hire date                                 |
| `PEOPLE_BADGE_NUMBER`                 | Badge number                              |
| `PEOPLE_FIRST_NAME`                   | First name                                |
| `PEOPLE_LAST_NAME`                    | Last name                                 |
| `PEOPLE_BIRTH_DATE`                   | Date of birth                             |
| `PEOPLE_PERSON_NUMBER`                | HR person number ("Employee ID")          |
| `PEOPLE_HOME_LABOR_CATEGORY`          | Home labour category                      |
| `PEOPLE_PAYRULE`                      | Pay rule name                             |
| `PEOPLE_ACCRUAL_PROFILE_NAME`         | Accrual profile                           |
| `PEOPLE_EXPECTED_DAILY_HOURS`         | Expected daily hours                      |
| `PEOPLE_EXPECTED_WEEKLY_HOURS`        | Expected weekly hours                     |
| `PEOPLE_EXPECTED_PAYPERIOD_HOURS_NEW` | Expected pay-period hours                 |

{% hint style="info" %}
The plugin first attempts to fetch all columns (core + extended). If the extended keys are not supported by your tenant, it falls back automatically to the core columns only so the stream always succeeds.
{% endhint %}

***

#### `timecards`

Raw timecard objects from the UKG timekeeping API. Each row represents one employee's timecard for a given date-chunk window.

**Sync strategies:** Full refresh, Incremental.\
**Primary key:** `id` (composite: `employee_qualifier|chunk_start_date`).\
**Cursor field:** `last_modified_timestamp`.

The nested UKG response structure is stored as-is (semi-structured). For row-level analysis, use the `timecard_spans` stream instead.

**Incremental behaviour**

On incremental syncs the plugin checks which employees had timecard changes since the previous sync. Only those employees are re-fetched, reducing API load significantly on large tenants.

**Delete detection**

When **Detect deletes via audit logs** is enabled, timecard records whose employee has a delete audit event in the sync window are emitted with a delete flag to Snowflake, instructing the Omnata runtime to remove that row from the destination table.

***

#### `timecard_spans`

The `timecard_spans` stream processes the same timecard data as the `timecards` stream and flattens it into **one row per span or pay-code edit**. This produces a much more query-friendly tabular shape.

**Sync strategies:** Full refresh, Incremental.\
**Primary key:** `id` / `WORKITEMID` — a 16-character deterministic hash based on `employee_qualifier + apply_date + pay_code + transaction_id`.\
**Cursor field:** `last_modified_timestamp`.\
**Depends on:** `timecards` (must be enabled in the same sync).

Two source types are merged into this single stream:

| Source                        | `IS_PAYCODE_EDIT` |
| ----------------------------- | ----------------- |
| Calculated spans (day totals) | `false`           |
| Manual pay-code edits         | `true`            |

Key columns:

| Column                                                     | Description                                           |
| ---------------------------------------------------------- | ----------------------------------------------------- |
| `id` / `WORKITEMID` / `UNIQUE_KEY`                         | Deterministic row identifier                          |
| `EMPLOYEE_NUMBER`                                          | Employee qualifier                                    |
| `APPLYDATE`                                                | Date the row applies to (YYYY-MM-DD)                  |
| `SPAN_START_DATE_TIME`                                     | Span start timestamp                                  |
| `SPAN_END_DATE_TIME`                                       | Span end timestamp                                    |
| `PUNCH_IN`                                                 | Earliest in-punch of the day                          |
| `PUNCH_OUT`                                                | Latest out-punch of the day                           |
| `PAY_CODE_NAME`                                            | Pay code qualifier                                    |
| `PAYCODETYPE`                                              | Pay code type                                         |
| `TOTAL_WORK_TIME_IN_HOURS`                                 | Hours for this span                                   |
| `DAYS`                                                     | Days for this span                                    |
| `ESTIMATED_WAGE`                                           | Estimated wage                                        |
| `WORKRULE`                                                 | Work rule qualifier                                   |
| `WORKED_JOB`                                               | Transferred job qualifier                             |
| `WORKED_COST_CENTER`                                       | Transferred cost centre qualifier                     |
| `SPAN_LABOR_CATEGORIES`                                    | Labour category qualifier                             |
| `IS_PAYCODE_EDIT`                                          | `true` when row comes from a manual pay-code edit     |
| `IS_HISTORICAL`                                            | `true` for historical correction entries              |
| `HISTORICAL_DATE`                                          | Date of the historical correction                     |
| `HISTORICAL_HOURS` / `HISTORICAL_DAYS` / `HISTORICAL_WAGE` | Historical correction amounts                         |
| `TIMECARD_TRANSACTIONID`                                   | UKG transaction ID (stable across syncs when present) |
| `FILEUPDATEDATETIME`                                       | Timecard last-modified timestamp                      |

***

#### `schedules`

Schedule data from the UKG scheduling API. Each shift is flattened into one row per segment; shifts with no segments produce a single row keyed on the shift itself.

**Sync strategies:** Full refresh, Incremental.\
**Primary key:** `id` / `TIMEITEMID` — segment ID from UKG or a deterministic hash.\
**Cursor field:** `last_modified_timestamp`.

Key columns:

| Column                         | Description                                   |
| ------------------------------ | --------------------------------------------- |
| `id` / `TIMEITEMID`            | Segment identifier                            |
| `SHIFTASSIGNID`                | Parent shift identifier                       |
| `EMPLOYEE_NUMBER`              | Employee qualifier                            |
| `APPLYDATE`                    | Date portion of segment start (YYYY-MM-DD)    |
| `SHIFT_SEGMENT_START_TIME`     | Segment start timestamp                       |
| `SHIFT_SEGMENT_END_TIME`       | Segment end timestamp                         |
| `PAYCODENAME`                  | Pay code qualifier                            |
| `PAYCODETYPE`                  | Pay code type                                 |
| `HOURS`                        | Segment duration in hours                     |
| `SHIFT_SEGMENT`                | 1-based segment index within its parent shift |
| `SHIFTTYPE`                    | Shift type from UKG                           |
| `TRANSFERED_COST_CENTER_CODE`  | Transferred cost centre                       |
| `TRANSFERED_JOB`               | Transferred job qualifier                     |
| `LABOR_CATEGORY`               | Labour category                               |
| `WORKRULE`                     | Work rule qualifier                           |
| `WORKED_SALARY_CLASSIFICATION` | Salary classification for the worked job      |
| `FILEUPLOADEDDATETIME`         | Parent shift last-modified timestamp          |

Deleted shifts are propagated to Snowflake so Omnata removes them from the destination table.

***

#### `workload_planner`

Staffing demand data from the UKG workload planner. One row is produced per `(location, date, span_name, job)` combination.

**Sync strategies:** Full refresh, Incremental.\
**Primary key:** `id` — a 16-character deterministic hash based on `location + date + span_name + job`.\
**Cursor field:** `last_modified_timestamp`.

**Location auto-discovery:** Rather than requiring you to enumerate locations manually, the plugin reads employees' primary organisation paths and derives the unique leaf location paths automatically before fetching workload planner data.

Key columns:

| Column            | Description                                                                |
| ----------------- | -------------------------------------------------------------------------- |
| `id`              | Deterministic row identifier                                               |
| `LOCATION`        | Organisation path used to scope the query                                  |
| `DATE`            | Date for this row (YYYY-MM-DD)                                             |
| `SPAN_NAME`       | Workload span name (e.g. `HSO 0800-1606`)                                  |
| `SPAN_TYPE`       | `WORKLOAD` or `COVERAGE`                                                   |
| `SPAN_START_TIME` | Span start time                                                            |
| `SPAN_END_TIME`   | Span end time                                                              |
| `JOB`             | Organisational job qualifier (full org path with role as the last segment) |
| `BUDGET`          | Budgeted headcount                                                         |
| `PLAN`            | Planned headcount                                                          |
| `ACTUAL`          | Scheduled (actual) headcount                                               |

***

### Incremental Sync

All streams except `employees` support incremental syncs. On each run:

1. The plugin uses the stored `last_modified_timestamp` state to determine what has changed since the previous sync.
2. For `timecards` and `timecard_spans`, only employees with timecard changes since the last sync are re-fetched, keeping API usage proportional to actual change volume.
3. Date ranges are chunked into a maximum of **30 days per API request** to avoid payload size limits.

***

### Troubleshooting

| Symptom                                                                           | Likely cause                                                     | Resolution                                                                        |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Connection test fails with an authentication error                                | Incorrect credentials or wrong Token URL / Realm                 | Double-check all eight connection fields against your UKG application settings    |
| Connection test fails with a permissions error                                    | Service account lacks read access to the timekeeping module      | Grant the service account appropriate API read permissions in UKG Pro             |
| `employees` stream returns core columns only, missing `PEOPLE_PERSON_NUMBER` etc. | Extended data dictionary keys not licensed/enabled on tenant     | The plugin falls back automatically; contact UKG support to enable extended keys  |
| `workload_planner` returns no rows                                                | No leaf location paths could be derived from employee org fields | Verify employees have primary organisation values with at least two path segments |
| Timeouts or server errors on large tenants                                        | Batch size too large                                             | Reduce **Fetch batch size** to `100`–`200`                                        |
| Deleted timecards not being removed from Snowflake                                | Delete detection is disabled                                     | Enable **Detect deletes via audit logs** in the sync configuration                |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.omnata.com/omnata-product-documentation/omnata-sync-for-snowflake/apps/ukg-pro.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
