How the Workday integration runs
Purpose of this document
This is the operational reference for the Deeper Signals ↔ Workday integration. It complements the Workday Integration Configuration Guide, which covers tenant-side setup. Where the configuration guide answers "what do I need to set up in Workday?", this document answers "what does Deeper Signals actually do at runtime, and when?"
Each lifecycle below is laid out the same way: a sequence diagram of the Workday calls involved, a metadata block (trigger, cadence, idempotency, observability), and a per-step explanation of what is being asked of Workday and why. The document is intended for tenant administrators and IT teams who need to understand integration traffic, monitor it, or troubleshoot it.
:::info Reference
For OAuth scopes, Domain Security Policies, and the Integration System User configuration referenced throughout, see Workday Integration Configuration Guide — Part 1. :::
High-level architecture
Deeper Signals runs four independent execution paths against your Workday tenant. Three are recurring polls driven by our scheduler; one is event-driven and fires when a candidate or worker finishes an assessment in our platform.
Figure 1 — Deeper Signals services and the Workday API surfaces they call. Solid arrows show traffic; dashed arrows show OAuth Bearer-token attachment.
Recurring jobs at a glance
The diagram below plots a one-hour window of recurring traffic. The four jobs run independently — they do not coordinate, and they may overlap. Each job acquires its own distributed lock so a slow run never causes the next tick to pile up.
Figure 2 — One hour of recurring traffic. Tick frequencies: assess-candidate poll 10 min, hired-candidate poll 15 min, worker sync 30 min; the OAuth token is fetched on demand before a job and cached about 1 h (drawn here at 20 min.
| Job | Cadence | Workday APIs touched | Section |
|---|---|---|---|
| OAuth access token | On demand, cached ~1 h | /ccx/oauth2/{tenant}/token | 1.1 |
| Assess-candidate poll | Every 10 minutes | SOAP Recruiting: Get_Assess_Candidate, Get_Candidates, Get_Job_Requisitions, Get_Organizations | 2.1 |
| Assessment completion write-back | On assignedAssessmentFinished | SOAP Recruiting: Assess_Candidate, Put_Candidate_Attachment; REST customObject/v2 | 2.2 |
| Hired-candidate poll | Every 15 minutes | SOAP Recruiting: Get_Candidates | 2.3 |
| Worker sync | Every 30 minutes | SOAP Human_Resources: Get_Workers | 3.1 |
| Worker write-back | On assignedAssessmentFinished (worker) | SOAP Staffing: Put_Worker_Document; REST customObject/v2 | 3.2 |
| Org hierarchy build | Tied to HRIS sync (30 min) | (internal — no Workday call) | 3.3 |
How to read the lifecycle pages
Each lifecycle in Parts 1--3 follows the same structure:
- Sequence diagram — every Workday call, in order, with the participant who initiated it.
- Lifecycle metadata — trigger, cadence, idempotency, and observability hook in a four-cell block.
- Step-by-step rationale — for each numbered step in the diagram, what we ask Workday for, what Workday returns, and why we need it.
The conventions used in the sequence diagrams:
| Notation | Meaning |
|---|---|
From_Moment / To_Moment | Workday SOAP timestamp request fields used to bound an incremental query. |
Updated_From / Updated_To | Variant of the bounded-time pattern, used by Get_Candidates. |
Transaction_Date_Range | Variant of the bounded-time pattern, used by Get_Workers. |
?bulk=true&updateIfExists=true | Idempotent REST upsert: insert if missing, overwrite if present, in a single batch. |
| Dashed arrow | Response from Workday back to Deeper Signals. |
opt / alt | Conditional branch executed only when the condition holds. |
Authentication lifecycle
1.1 OAuth 2.0 access token (on demand)
All Workday calls — SOAP and REST alike — are authenticated with a short-lived Bearer access token. Deeper Signals exchanges the long-lived refresh token (configured once on the API Client for Integrations) for a fresh access token on demand, caches it per integration and renews it shortly before it expires. It caches the result in-process for all four execution paths to share.
| Trigger First Workday call of a job | Cadence On demand; cached until ~1 minute before expiry | Idempotency Safe — a new token can be issued at any time | Observability Authentication errors in the integration log |
Why we refresh ahead of expiry
Workday access tokens expire after one hour. Renewing a cached token a minute before expiry guarantees that no in-flight job holds an expired token, even if a single tick is delayed by lock contention or a slow Workday response. The refresh is a single POST with the refresh-token grant type; it does not consume any business-data quota.
POST /ccx/oauth2/{tenant}/token HTTP/1.1
Host: {tenant}.workday.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&client_id={client-id}&client_secret={client-secret}&refresh_token={non-expiring-refresh-token}
:::warning If the token request fails
The job that needed the token logs the authentication error and stops; the next scheduled tick tries again. The most common cause is a rotated client secret — see Configuration Guide §1.6. :::
ATS lifecycles (Recruiting)
2.1 Assess-candidate poll (every 10 minutes)
This is the entry point of the ATS integration. It detects candidates who have just been moved into a Deeper Signals assessment step in Workday Recruiting and bootstraps everything we need on our side: candidate, recruiter, requisition, and supervisory-organisation manager.
| TriggerSymfony Scheduler tick | CadenceEvery 10 minutes | IdempotencyIdempotent: parked events without a Reference ID are retried each tick | ObservabilityAssess-poll lag |
Figure 3 — Per-tick flow when one or more candidates are detected in a Deeper Signals assessment step. The ATS→HRIS lane (single-worker sync) is internal to Deeper Signals — no additional Workday call is made on that arrow.
Step-by-step rationale
Step
Workday call
Why we make it
2 -- 3
Get_Assess_Candidate with From_Moment / To_Moment equal to last poll → now
Returns every candidate that entered an Assess Candidate step since our last successful tick. This is the primary detection hook.
4
(no call — local park)
If Workday has not yet stamped an Assessment_Test_Reference on the event (sometimes lags a few seconds in Workday), we park it in an internal retry queue and re-evaluate on the next tick. Without the reference we cannot match the event to a configured Recruiting Assessment Test.
5 -- 6
Get_Candidates by candidate ID
Returns the candidate's email address, first name, last name and Workday identifiers (Candidate_ID, WID, and the referenced job requisition). Email is required to invite the candidate and is the key we match them on; the names personalise the invitation and the assessment intro. No other personal data is read from the candidate record — see the note below this table.
7 -- 8
Get_Job_Requisitions by requisition WID
Returns the recruiter reference, the supervisory organisation reference, and the job title. The title is folded into the campaign name — this happens on every deployment. Resolving the recruiter reference to a person, and making them the campaign owner, requires an HRIS integration; in an ATS-only deployment the reference is read but never resolved.
9 -- 10
Get_Organizations by Sup. Org WID
HRIS deployments only. Returns the supervisory organisation's manager, who is added to the same single-worker sync as the recruiter so that team analytics see them later. This call is gated on an HRIS integration being configured and is not made in an ATS-only deployment.
11
(no call — internal HRIS sync)
HRIS deployments only. The recruiter and Sup. Org manager are upserted into our user directory, making the campaign assignable to a real DS account. In an ATS-only deployment no worker record is read or created, and the campaign is created without an auto-assigned owner.
12
(no call — campaign clone)
We resolve or clone the campaign template named Workday - <Title> - <ReqID>. Cloning preserves the recruiter as owner and reuses any custom intro text from the configured template.
13
(no call — outbound email)
The candidate receives the assessment invitation. From this point on, control is theirs until completion.
:::warning Candidate personal data — the complete list
The only fields Deeper Signals reads from a Workday candidate record are: email address, first name, last name, and the Workday identifiers used for correlation (Candidate_ID, Candidate WID, Applicant_ID, Worker_ID once hired, and the referenced job requisition ID/WID).
Not requested, not read, not stored: phone number, mobile number, postal address, country, nationality, date of birth, gender, ethnicity, disability status, veteran status, salary or compensation data, résumé/CV content, application questionnaire responses, and social media identifiers. Earlier revisions of this document listed phone and country among the fields read by Get_Candidates; that was a generic ATS capability description and was never accurate for the Workday integration — no such fields exist on the internal candidate model.
:::
:::info Why we resolve the manager — and when we don't
Hiring managers occasionally complete the same assessment as part of calibration. By syncing the Sup. Org manager up front, we avoid a "phantom user" later if they are tagged as the assessment reviewer.
Steps 9--11 run only when an HRIS integration is configured. An ATS-only deployment skips them entirely: Get_Organizations is never called, no worker data is read, and the corresponding Workday security domains are not required. See §1.3 of the Integration Guide for the reduced ATS-only permission set.
:::
2.2 Assessment completion write-back (event-driven)
When a candidate finishes the Deeper Signals assessment in our platform, we publish an internal event that fans out to every integration tagged on the candidate's invitation. The Workday handler runs once per Workday-tagged invitation and writes three artefacts back to the Job Application: the human-readable status & score, the rendered PDF report, and the per-dimension custom-object scores.
TriggerInternal event assignedAssessmentFinished | CadenceOnce per completed assessment | IdempotencyREST upsert is idempotent; SOAP write is retried with replay key | ObservabilityPer-message handler log + DLQ on failure |
Figure 4 — From candidate completion to three artefacts on the Job Application.
Step-by-step rationale
| Step | Workday call | Why we make it |
|---|---|---|
| 1 -- 3 | (no Workday call yet) | Internal: completion event is dispatched on the message bus and routed to the Workday handler if the candidate has any invitations tagged with this integration. |
| 4 -- 5 | Assess_Candidate with status, score, results URL | Updates the Job Application's assessment status to Deepersignals_Completed and stamps the recruiter-visible score. This is what flips the candidate visually in Workday's recruiter UI. |
| 6 | (no call — local PDF render) | We render the candidate's report PDFs (Core Drivers and, where licensed, Drivers Pro) so they can be attached in the next step. The render is fully local; no Workday data is needed. |
| 7 -- 8 | Put_Candidate_Attachment with category OTHER_DOCUMENTS | Attaches the rendered PDF report(s) to the Job Application. OTHER_DOCUMENTS is a Workday-owned category — it does not need to be created in your tenant. |
| 9 -- 10 | POST /api/customObject/v2/candidateDeeperSignal…?bulk=true&updateIfExists=true | Writes the per-dimension scores into the custom object so they can be reported on, filtered, and surfaced in Workday Reporting alongside other recruitment data. The bulk + updateIfExists flags make the call idempotent — replays will not duplicate rows. |
:::warning Order matters
The status update (step 4) is performed before the PDF and custom-object writes. If the recruiter UI flips to Completed but the artefacts have not appeared yet, expect a one- to two-second gap while the bulk REST call settles. :::
2.3 Hired-candidate poll (every 15 minutes)
Once a candidate is hired in Workday, they get a Worker_ID and stop being "just a candidate" — they become a worker. This poll detects that transition for every candidate we've already invited and merges their candidate identity with the worker identity coming from the HRIS sync.
| TriggerSymfony Scheduler tick | CadenceEvery 15 minutes | IdempotencyIdempotent: re-running re-evaluates the same window without duplicating writes | ObservabilityHired-poll lag |
Figure 5 — Detecting candidate → worker transition and re-firing assessment write-back to the worker side.
Step-by-step rationale
| Step | Workday call | Why we make it |
|---|---|---|
| 3 -- 4 | Get_Candidates with Updated_From / Updated_To | Returns every candidate whose record has changed since the last hired-poll tick. We filter the response client-side for those that now carry a Worker_ID. |
| 5 | (no call — local lookup) | Find every prior assessment invitation that points at this candidate. These are the records of every assessment they've already received from us. |
| 6 | (no call — duplicate resolution) | If the HRIS sync has already created a placeholder worker for this person (typical when there's a small clock skew between recruiting and HCM), we merge the placeholder into the candidate's user account — a candidate-to-worker reconciliation step. |
| 7 | (no call — local mark) | Mark every prior invitation "HIRED" and store the Worker_ID on the user account. From this point on, the user is treated as a worker, not a candidate. |
| 8 -- 9 | Re-dispatch the completion event | If the HRIS integration is also active, we re-fire the completion write-back for every assessment they previously finished — but this time the handler runs against the worker side (Part 3.2), not the candidate side. This is what makes pre-hire assessments visible on the new hire's worker profile. |
:::info Two integrations, one event source
Steps 8--9 illustrate the design principle behind Deeper Signals' Workday integrations: the same internal completion event drives both candidate and worker write-back. The candidate version fires immediately on completion (Part 2.2); the worker version fires retroactively at hire time. :::
HRIS lifecycles (Workers)
3.1 Worker sync (every 30 minutes)
The worker sync is the spine of the HRIS integration. It pulls every worker that has changed since the last successful tick, applies your field mapping, upserts the user in our directory, captures the manager relationship, and feeds team and hierarchy services.
| TriggerSymfony Scheduler tick | CadenceEvery 30 minutes (incremental) | IdempotencyUpsert by email; rerunning a window has no side effects | ObservabilityHRIS sync lag |
Figure 6 — Per-tick flow of the incremental worker sync. Pagination is at 50 workers per page; an integration lock prevents two ticks from overlapping.
Step-by-step rationale
| Step | Workday call | Why we make it |
|---|---|---|
| 2 -- 3 | (no call — lock acquisition) | The integration acquires its distributed lock so a slow tick cannot overlap with the next one. The lock is per-tenant. |
| 4 | (no call — window calculation) | The query window is the previous sync timestamp → now. On the first run we use a configured backfill horizon; on subsequent runs we are fully incremental. |
| 5 -- 6 | Get_Workers with Transaction_Date_Range | Returns one page of workers (max 50). Workday paginates server-side; we follow until exhausted. Using the transaction-date range means we only ever transfer workers whose data actually changed. |
| 7 -- 11 | (no calls — per-worker processing) | For each worker on the page: apply field mapping, upsert the Deeper Signals user keyed on email, persist the worker's HRIS dataset row, capture the Workday Manager WID, and — if a termination date is now set — mark the user inactive. |
| 12 -- 14 | (no Workday call — internal team service) | For every synced worker we update an internal team index keyed off the captured manager relationship — the worker is added as a member of their manager's team. Empty teams are removed in a sweep at the end. |
| 15 -- 16 | (no Workday call — internal visibility-index rebuild) | The org-hierarchy build (covered in §3.3) runs after the page loop completes. This is what makes manager-of-manager visibility work in our admin UIs. |
| 17 -- 18 | (no Workday call — search-index update) | The HRIS dataset is indexed for search so that team analytics, filters, and saved views remain fast on tenants with hundreds of thousands of workers. |
:::info Why incremental, not full
A full pull on a 50,000-worker tenant takes minutes and is rate-limited by Workday's per-tenant integration quota. Incremental sync uses two timestamps and is typically <30 seconds even on large tenants. :::
3.2 Worker write-back (event-driven)
Counterpart to §2.2 on the worker side. When a worker (or a recently-hired candidate, see §2.3) finishes a Deeper Signals assessment, we attach the rendered PDF to their worker record and write the per-dimension scores to the worker's custom object.
TriggerInternal event assignedAssessmentFinished | CadenceOnce per completed assessment (worker side) | IdempotencyREST upsert is idempotent; SOAP write is retried with replay key | ObservabilityPer-message handler log + DLQ on failure |
Figure 7 — Worker write-back. The custom object is only visible on the worker profile if it is added to a Profile Group.
Step-by-step rationale
| Step | Workday call | Why we make it |
|---|---|---|
| 1 -- 2 | (no Workday call yet) | Internal: completion event is dispatched and routed to the worker write-back handler. Conditions: the user must have a worker_id and the HRIS integration must be active. |
| 3 | (no call — local PDF render) | Render the worker-facing PDF report. Same engine as the candidate report, different recipient. |
| 4 -- 5 | Put_Worker_Document with category ASSESSMENT | Attaches the report to the worker record. ASSESSMENT is a tenant-owned Document_Category_ID — it must exist in your tenant. See Configuration Guide §3.3. |
| 6 -- 7 | POST /api/customObject/v2/workerDeeperSignal…?bulk=true&updateIfExists=true | Writes per-dimension scores. The custom object only renders on the worker profile if it has been added to a Profile Group — that's a tenant configuration step, not a per-call action. |
3.3 Org hierarchy build (after every sync)
At the end of every successful HRIS sync, Deeper Signals rebuilds an internal manager-of-manager visibility index from the per-worker Manager_Reference captured during sync. This is what makes admin queries like "show me everyone Anna can see, including her direct reports' reports" return in milliseconds. The build is a full rebuild on every tick, runs entirely on the Deeper Signals side, and triggers no additional Workday calls.
| TriggerEnd of every HRIS sync | CadenceTied to sync (30 min) | IdempotencyFull rebuild, no incremental state | ObservabilityBuild duration logged per account |
Full API call inventory
Every Workday operation Deeper Signals invokes, grouped by service and surface.
SOAP — Recruiting v45.2
| Operation | Used by | Purpose |
|---|---|---|
Get_Assess_Candidate | 2.1 | Detect candidates entering an Assess Candidate step. |
Get_Candidates | 2.1, 2.3 | Resolve candidate email, first and last name, and Workday identifiers (2.1); detect hires by Worker_ID transition (2.3). |
Get_Job_Requisitions | 2.1 | Resolve recruiter, supervisory organisation, and title. |
Get_Organizations | 2.1 | Resolve the supervisory organisation's manager. |
Assess_Candidate | 2.2 | Write status, score, and results URL back to the Job Application. |
Put_Candidate_Attachment | 2.2 | Attach the rendered PDF report to the Job Application. |
SOAP — Staffing v45.2
| Operation | Used by | Purpose |
|---|---|---|
Put_Worker_Document | 3.2 | Attach the rendered PDF report to the worker record. |
SOAP — Human_Resources v45.2
| Operation | Used by | Purpose |
|---|---|---|
Get_Workers | 3.1 | Incremental worker pull driven by Transaction_Date_Range. |
REST — customObject v2
| Operation | Used by | Purpose |
|---|---|---|
POST /api/customObject/v2/candidateDeeperSignal…?bulk=true&updateIfExists=true | 2.2 | Idempotent upsert of per-dimension scores on the Job Application custom object. |
POST /api/customObject/v2/workerDeeperSignal…?bulk=true&updateIfExists=true | 3.2 | Idempotent upsert of per-dimension scores on the worker custom object. |
OAuth — Token endpoint
| Operation | Used by | Purpose |
|---|---|---|
POST /ccx/oauth2/{tenant}/token | 1.1 | Exchange the long-lived refresh token for a one-hour access token, every 20 minutes. |
Triggers, cadence, and idempotency
One-page cheat sheet across all six lifecycles.
| Lifecycle | Trigger | Cadence | Idempotent? | Failure mode |
|---|---|---|---|---|
| OAuth access token (§1.1) | Any job | On demand, cached ~1 h | Yes | The calling job logs the error and stops; the next tick retries. |
| Assess-candidate poll (§2.1) | Scheduler | Every 10 min | Yes — parked events retried each tick | Tick logged with error; next tick re-scans the same window. |
| Completion write-back (§2.2) | Event | Per assessment | REST yes; SOAP via replay key | Goes to DLQ; retried by ops or replayed manually. |
| Hired-candidate poll (§2.3) | Scheduler | Every 15 min | Yes | Same window re-scanned next tick. |
| Worker sync (§3.1) | Scheduler | Every 30 min (incremental) | Yes — upsert by email | Lock released, next tick covers the missed window. |
| Worker write-back (§3.2) | Event | Per assessment (worker side) | REST yes; SOAP via replay key | Goes to DLQ; retried or replayed. |
| Hierarchy build (§3.3) | After sync | 30 min (tied to sync) | Yes — full rebuild | Old closure table preserved if rebuild fails mid-flight. |
:::note Closing note
This document is a living reference and tracks the integration as it currently runs in production. If you spot a divergence between what's described here and what you observe in your tenant, raise it with your Deeper Signals contact and we'll reconcile it in the next revision. :::