HDL Transformation Fast Formula — Part 1: OPERATION Routing, METADATA Arrays & WSA Caching

Step-by-step walkthrough of an Oracle HCM Cloud HDL Transformation Fast Formula covering OPERATION routing, METADATA arrays, MAP steps, WSA caching, LINEREPEATNO, and ElementEntry output.

Vendor Deduction Interface | ElementEntry + ElementEntryValue

This is Part 1 of a 3-part series on HDL Transformation Formulas. This post covers the concepts end-to-end — what each section of the formula does and why. No code to copy-paste here. Just the understanding you need before writing a single line.

HDL Transformation Formula Series

Pure Concepts ← You are here

What each section of the formula does. INPUTS, OPERATION, METADATA, MAP (5 steps), WSA, LINEREPEATNO, RETURN. Zero code to memorize — just understanding.

Code Walkthrough Coming soon

The actual formula code, explained line-by-line. Value set definitions, WSA implementation, date conversions, ISNULL patterns, ESS_LOG_WRITE debugging. Moderate complexity — you’ll be able to read any HDL formula after this.

Build Your Own Coming soon

Full implementation guide. Setting up the formula in Oracle, creating the value sets, configuring the HDL integration, testing with real data, debugging production issues. Copy-paste ready.

Abhishek Mohanty

Before we go section by section, here’s what this formula does end to end:

Vendor CSV File**SSN, Date, Code, Amounts

HDL Transformation This Formula

ElementEntry .dat Header + Value rows

A third-party benefits administration vendor (BenAdmin) sends a CSV with deduction and employer contribution amounts. This formula transforms each row into Oracle HDL format — resolving SSNs to Assignment Numbers, mapping vendor codes to Oracle Element Names, managing MultipleEntryCount, and generating both ElementEntry (header) and ElementEntryValue (detail) rows.


The vendor manages employee benefit enrollments — medical, dental, vision, life insurance, FSA, HSA, loans. Every pay period, they send a flat CSV file with deduction details to load into Oracle as Element Entries.

The Raw Input File Layout

Each row in the vendor file maps to one set of delimited columns. The HDL engine reads these into POSITION variables:

ColumnPositionDescriptionExample
SSNPOSITION1Employee Social Security Number123-45-6789
EFFECTIVE_DATEPOSITION2Date the deduction applies (YYYY-MM-DD)2024-01-15
BENEFIT_PLAN_CODEPOSITION3Vendor’s internal code for the benefit planDENTAL01
DEDUCTION_TYPEPOSITION4Controls LINEREPEATNO branches and how many input values are loadedLOAN, PRE, POST, CU
AMOUNTPOSITION5Deduction amount (InputValueName = ‘Amount’)150.00
PERIOD_TYPEPOSITION6Period type for the deduction(varies)
PERCENTAGEPOSITION7Percentage for PRE/POST type deductions(blank or value)
LOAN_NUMBERPOSITION8Loan number (LOAN type only)(blank or value)
POSITION9–10POSITION9–10Reserved / additional fields(varies)
STATUSPOSITION11C = Cancel/End-date, blank = Active/New(blank)

Key point:** POSITION4 (Deduction Type) is the most important field after SSN and Date. It controls the formula’s branching logic — which LINEREPEATNO passes execute, which input values get loaded (Amount, Period Type, Percentage, Loan Number, Total Owed, Deduction Amount), and even whether the formula generates output on certain passes. A LOAN type deduction goes through 7 passes. A regular deduction goes through fewer.

How One Vendor Row Becomes Multiple Input Values

A single vendor row carries multiple amounts for the same deduction. The formula uses LINEREPEATNO to load each input value in a separate pass. For a LOAN type deduction, one source row generates up to 7 output rows:

/* One vendor row: */
123-45-6789,2024-01-15,DENTAL01,LOAN,150.00,Monthly,5.5,LN-001,,,,

/* Formula generates (up to 7 passes): */
Pass 1 (LINEREPEATNO=1): ElementEntry header
Pass 2 (LINEREPEATNO=2): ElementEntryValue → Amount = 150.00
Pass 3 (LINEREPEATNO=3): ElementEntryValue → Period Type = Monthly
Pass 4 (LINEREPEATNO=4): ElementEntryValue → Loan Number = LN-001
Pass 5 (LINEREPEATNO=5): ElementEntryValue → Total Owed = ...
Pass 6 (LINEREPEATNO=6): ElementEntryValue → Percentage = 5.5
Pass 7 (LINEREPEATNO=7): ElementEntryValue → Deduction Amount = ...

Not every deduction type needs all 7 passes. The formula checks POSITION4 on each pass — if the type doesn’t apply (e.g. Percentage only runs for PRE/POST types), it returns LINEREPEAT = 'Y' with no output, effectively skipping that pass.

Understanding MultipleEntryCount

Oracle HCM draws a fundamental distinction between recurring and non-recurring elements when it comes to MultipleEntryCount:

Monthly salary, standing allowance

MultipleEntryCount is not required as a key when using SourceSystemId.

“You don’t need to supply the MultipleEntryCount attribute as source keys to uniquely identify the records.” — Oracle Docs

Benefits deductions (our vendor elements)

MultipleEntryCount must be incremented for each entry of the same assignment + element within the same payroll period.

“You must increment the value of MultipleEntryCount for each entry of the same assignment and element.” — Oracle Docs

The vendor interface loads non-recurring elements that allow multiple entries. This means the formula must query the cloud for the current highest MultipleEntryCount before assigning the next one — and track assigned values across rows within the same batch using WSA.

Key Takeaway: Three benefit plan rows (Dental, Medical, Vision) for the same employee map to three different elements, so they each get independent entries with their own MultipleEntryCount. MultipleEntryCount is needed when the same non-recurring element requires multiple entries for the same assignment within the same payroll period.


The vendor file gives us an SSN and an Vendor Deduction Code. Oracle HCM needs an Assignment Number and an Oracle Element Name. These are completely different identifiers in completely different systems. Value Sets act as the bridge — SQL-backed lookup functions that run inside the Fast Formula engine.

VENDOR ENVIRONMENT

SSN: 123-45-6789

Code: DENTAL01

VALUE SETS

ORACLE WORLD

Asg#: E12345

Element: Dental EE Deduction

The formula uses 11 value sets. Here’s what each one does:

#Value SetWhat It DoesReturns
1XXVA_DEDUCTION_CODESMaps vendor plan code (DENTAL01) to Oracle Element NameElement Name
2XXVA_DEDUCTION_CODES_INPUTGets Input Value Name for the element (e.g. Amount)Input Value Name
3XXVA_GET_LATEST_ASSIGNMENT_NUMBERResolves SSN + date → Assignment NumberAssignment# (E12345)
4XXVA_GET_PERSON_NUMBERResolves SSN → Person NumberPerson# (100045)
5MAX_MULTI_ENTRY_COUNTGets highest existing MultipleEntryCount for Person+Element+DateMax count (or NULL)
6–7GET_ELEMENT_ENTRY_SOURCE_SYSTEM_ID / _OWNERRetrieves existing SourceSystemId/Owner for MERGE key reuseExisting SSID/SSO
8–9GET_ELEMENT_ENTRY_VALUE_SOURCE_SYSTEM_ID / _OWNERSame but at Element Entry Value levelElementEntryValue-level SSID/SSO
10GET_ELEMENT_ENTRY_START_DATEFor Cancel rows — gets original start dateOriginal start date
11GET_ELEMENT_ENTRY_INPUT_START_DATESame but at ElementEntryValue level (date-tracked scenarios)ElementEntryValue original start date

Translate vendor codes → Oracle element names. Called once per row regardless. No caching benefit.

Resolve SSN/Person data. Same SSN appears across multiple rows — WSA caching saves significant performance here.


INPUTS ARE OPERATION (TEXT),
LINEREPEATNO (NUMBER),
LINENO (NUMBER),
POSITION1 (TEXT), POSITION2 (TEXT), POSITION3 (TEXT),
POSITION4 (TEXT), POSITION5 (TEXT), POSITION6 (TEXT),
POSITION7 (TEXT), POSITION8 (TEXT),
POSITION9 (TEXT), POSITION10 (TEXT), POSITION11 (TEXT)

DEFAULT FOR LINENO IS 1
DEFAULT FOR LINEREPEATNO IS 1
DEFAULT FOR POSITION1 IS 'NO DATA'
DEFAULT FOR POSITION2 IS 'NO DATA'
/* ... same for POSITION3 through POSITION11 ... */
VariableWhat It Does
OPERATIONThe HDL engine calls the formula multiple times with different values: FILETYPE, DELIMITER, READ, NUMBEROFBUSINESSOBJECTS, METADATALINEINFORMATION, then MAP per row. The formula is a router.
LINEREPEATNOThe repeat counter. When formula sets LINEREPEAT = 'Y', HDL re-invokes for the same row with incremented LINEREPEATNO. One input row can generate up to 7 HDL output rows: 1 ElementEntry header (pass 1) + up to 6 ElementEntryValue rows (passes 2–7), one per input value (Amount, Period Type, Loan Number, Total Owed, Percentage, Deduction Amount). The deduction type (POSITION4) controls how many passes run.
LINENOLine number from the source file (1-based). Useful for error tracing.
POSITION1–11Map directly to CSV columns in order. HDL engine splits each line by delimiter and populates these.

Why DEFAULT FOR is required: When HDL calls the formula for non-MAP operations (like FILETYPE), the POSITION variables aren’t populated — no source row is being processed. Without defaults, the formula throws a null reference error at runtime.


InputHDL engine asks: “What file type? What delimiter? How many objects?”
Formula returnsDELIMITED, comma, NONE, 2
HDL outputNothing written to .dat yet — engine is just being configured
IF OPERATION = 'FILETYPE' THEN
    OUTPUTVALUE = 'DELIMITED'
ELSE IF OPERATION = 'DELIMITER' THEN
    OUTPUTVALUE = ','
ELSE IF OPERATION = 'READ' THEN
    OUTPUTVALUE = 'NONE'
ELSE IF OPERATION = 'NUMBEROFBUSINESSOBJECTS' THEN
(
    OUTPUTVALUE = '2'
    RETURN OUTPUTVALUE
)

FILETYPE**DELIMITED

DELIMITER ,

READ NONE

OBJECTS 2

METADATA

MAP (per row)

OperationEngine AsksOur AnswerWhy
FILETYPE”What kind of file?”DELIMITEDOnly valid option for HDL transformation
DELIMITER”What separates values?“,the vendor sends CSV. Default is pipe (|), so we override.
READ”Skip header rows?”NONEvendor file has no header row — process every line.
NUMBEROF…”How many HDL objects?“2ElementEntry (header) + ElementEntryValue (detail with amount)

InputHDL engine asks: “What columns does each object have?”
Formula returnsMETADATA1[ ] array, METADATA2[ ] array
HDL writes to .datMETADATA|ElementEntry|LegislativeDataGroupName|EffectiveStartDate|…METADATA|ElementEntryValue|LegislativeDataGroupName|EffectiveStartDate|…

After the setup handshake, the HDL engine calls the formula with OPERATION = 'METADATALINEINFORMATION'. This is where the formula defines the column headers** for the .dat output file. These become the METADATA rows you see at the top of each block in the .dat file.

The .dat File Has Two METADATA Header Rows

Since we told the engine NUMBEROFBUSINESSOBJECTS = 2 (ElementEntry + ElementEntryValue), the formula must define two METADATA arrays — one per object. These become the two header rows in the .dat file:

/* This header row in the .dat file: */
METADATA|ElementEntry|LegislativeDataGroupName|EffectiveStartDate|ElementName|AssignmentNumber|CreatorType|EffectiveEndDate|EntryType|MultipleEntryCount

/* Is generated by this code: */

The Code — METADATA1 (ElementEntry Header)

The formula uses an array variable called METADATA1. Each array position maps to a column in the .dat header. Positions [1] and [2] are reserved by the HDL engine for FileName and FileDiscriminator — the formula starts filling from position [3].

ELSE IF OPERATION = 'METADATALINEINFORMATION' THEN
(
    /* ================================================= */
    /* METADATA1 — ElementEntry column definitions        */
    /* [1] = FileName (auto-filled by HDL engine)         */
    /* [2] = FileDiscriminator (auto-filled by HDL engine)*/
    /* [3] onwards = we define                            */
    /* ================================================= */

    METADATA1[3]  = 'LegislativeDataGroupName'
    METADATA1[4]  = 'EffectiveStartDate'
    METADATA1[5]  = 'ElementName'
    METADATA1[6]  = 'AssignmentNumber'
    METADATA1[7]  = 'CreatorType'
    METADATA1[8]  = 'EffectiveEndDate'
    METADATA1[9]  = 'EntryType'
    METADATA1[10] = 'MultipleEntryCount'
    METADATA1[11] = 'SourceSystemOwner'
    METADATA1[12] = 'SourceSystemId'
    METADATA1[13] = 'ReplaceLastEffectiveEndDate'

The HDL engine reads this array and writes the following row to the .dat file:

METADATA|ElementEntry|LegislativeDataGroupName|EffectiveStartDate|ElementName|AssignmentNumber|CreatorType|EffectiveEndDate|EntryType|MultipleEntryCount|SourceSystemOwner|SourceSystemId|ReplaceLastEffectiveEndDate

The Code — METADATA2 (ElementEntryValue Header)

Same pattern. METADATA2 array defines the columns for the ElementEntryValue block:

    /* ================================================= */
    /* METADATA2 — ElementEntryValue column definitions   */
    /* ================================================= */

    METADATA2[3]  = 'LegislativeDataGroupName'
    METADATA2[4]  = 'EffectiveStartDate'
    METADATA2[5]  = 'ElementName'
    METADATA2[6]  = 'AssignmentNumber'
    METADATA2[7]  = 'InputValueName'              /* ← changes per pass */
    METADATA2[8]  = 'EffectiveEndDate'
    METADATA2[9]  = 'EntryType'
    METADATA2[10] = 'MultipleEntryCount'
    METADATA2[11] = 'ScreenEntryValue'            /* ← the actual value */
    METADATA2[12] = '"ElementEntryId(SourceSystemId)"'  /* parent link */
    METADATA2[13] = 'SourceSystemOwner'
    METADATA2[14] = 'SourceSystemId'
    METADATA2[15] = 'ReplaceLastEffectiveEndDate'

    RETURN METADATA1, METADATA2
)

This generates the second header row in the .dat file:

METADATA|ElementEntryValue|LegislativeDataGroupName|EffectiveStartDate|ElementName|AssignmentNumber|InputValueName|EffectiveEndDate|EntryType|MultipleEntryCount|ScreenEntryValue|ElementEntryId(SSID)|SourceSystemOwner|SourceSystemId|ReplaceLastEffectiveEndDate

How METADATA Links to RETURN in Sections 7 and 8

The column names in the METADATA arrays directly map to the named output variables in the formula’s RETURN statement. Here’s the connection:

/* METADATA defines the column header: */
METADATA1[5] = 'ElementName'

/* In the MAP block, the formula assigns the named variable: */
ElementName  = l_ElementName              /* = 'Dental EE Deduction' */

/* And includes it in the RETURN: */
RETURN ..., ElementName, ...

/* Result in .dat file:                                                */
/* METADATA |ElementEntry|...|ElementName                    |...      */
/* MERGE    |ElementEntry|...|Dental EE Deduction   |...      */
/*                             ↑ matched by variable name             */

The mapping rule:

METADATA1[N] defines column N header name for object 1 (ElementEntry)** In the MAP block, you assign a variable with that exact same name** and include it in the RETURN statement**

The HDL engine matches the RETURN variable name to the METADATA column name and writes the value into the correct position in the .dat file. The FileDiscriminator value ('ElementEntry' vs 'ElementEntryValue') tells the engine which METADATA block to use.


The reference vendor row used in all examples below:

Vendor Input Row

POSITION1SSN123-45-6789
POSITION2Effective Date2024-01-15
POSITION3Benefit PlanDENTAL01
POSITION4Deduction TypePRE
POSITION5Amount150.00
POSITION6Period TypeMonthly
POSITION7Percentage5.5
POSITION8Loan NumberLN-001
POSITION11Status(blank = Active)

This is the heart of the formula. When the HDL engine reaches a source row, it calls OPERATION = 'MAP'. The formula receives the raw CSV data in POSITION1–11 and must return Oracle HDL attributes. Five steps run in sequence.

Here’s what the formula needs to figure out for each row:

QuestionVendor Gives UsOracle NeedsStep
What type of deduction?POSITION4 (Deduction Type)PRE / POST / LOAN / CUStep 1
Which Oracle Element?DENTAL01Dental EE DeductionStep 2
Which employee?123-45-6789 (SSN)E12345 (Assignment#)Step 3
How many entries already exist?(doesn’t know)MultipleEntryCount = 2Step 4
New or existing entry?(doesn’t know)SourceSystemId for MERGEStep 5

STEP 1 Element Type

STEP 2 Element Lookup

STEP 3 Person / Assignment

STEP 4 MultipleEntryCount

STEP 5 SourceSystemId

Step 1: Read Input Values from POSITION Fields

POSITION4 = PRE→ l_DeductionType = ‘PRE’
POSITION5 = 150.00→ l_Amount = ‘150.00’
POSITION6 = Monthly→ l_PeriodType = ‘Monthly’
POSITION7 = 5.5→ l_Percentage = ‘5.5’
POSITION8 = LN-001→ l_LoanNumber = ‘LN-001’

The formula reads the deduction type from POSITION4 and the amount from POSITION5. It also captures other input values (Period Type, Percentage, Loan Number) from their respective positions for later LINEREPEATNO passes:

/* Read the key fields from the vendor row */
l_DeductionType    = TRIM(POSITION4)     /* 'PRE', 'POST', 'LOAN', 'CU' */
l_Amount           = TRIM(POSITION5)     /* '150.00' */
l_PeriodType       = TRIM(POSITION6)     /* 'Monthly' */
l_Percentage       = TRIM(POSITION7)     /* '5.5' (PRE/POST only) */
l_LoanNumber       = TRIM(POSITION8)     /* 'LN-001' (LOAN only) */

After this step: l_DeductionType = 'PRE' and l_Amount = '150.00'

Step 2: Resolve Element Name from Benefit Plan Code

POSITION3 = DENTAL01→ Value Set lookup
→ l_ElementName = ‘Dental EE Deduction’
→ l_InputValueName = ‘Amount’

The vendor uses its own benefit plan codes (DENTAL01, MEDICAL01, VISION01). Oracle doesn’t know these codes. The formula passes the vendor code to two value sets that translate it into Oracle terms:

/* Step 2: Translate vendor plan code → Oracle Element Name */

L_VendorPayCode = TRIM(POSITION3)
/* e.g. 'DENTAL01' */

/* Value set 1: vendor code → Oracle Element Name */
l_ElementName = GET_VALUE_SET('XXVA_DEDUCTION_CODES',
    '|=P_PAY_CODE=''' || L_VendorPayCode || '''')
/* 'DENTAL01' → 'Dental EE Deduction' */

/* Value set 2: vendor code → Input Value Name */
l_InputValueName = INITCAP(GET_VALUE_SET('XXVA_DEDUCTION_CODES_INPUT',
    '|=P_PAY_CODE=''' || L_VendorPayCode || ''''))
/* 'DENTAL01' → 'Amount' */

These are code-based** lookups — the value set definition maps each vendor code to its Oracle element. No person data is involved, so no WSA caching is needed here.

Value Set Translation

Vendor Code (POSITION3)Oracle Element NameInput Value Name
DENTAL01Dental EE DeductionAmount
MEDICAL01Medical EE DeductionAmount
VISION01Vision EE DeductionAmount

This mapping is defined in the value set configuration — not in the formula code. Adding a new benefit plan just means adding a row to the value set.

After Step 2: l_ElementName = 'Dental EE Deduction' and l_InputValueName = 'Amount'

Step 3: Resolve Person & Assignment

POSITION1 = 123-45-6789→ GET_VALUE_SET → L_PersonNumber = ‘100045’
POSITION2 = 2024-01-15→ GET_VALUE_SET → l_AssignmentNumber = ‘E12345’

Oracle HDL doesn’t understand SSN. It needs two things: Person Number and Assignment Number. Step 3 translates one into the other.

VENDOR FILE GIVES US

123-45-6789

SSN (POSITION1)

Value Set**calls DB

ORACLE HDL NEEDS

Person# 100045

Assignment# E12345

Two value sets do this translation:

XXVA_GET_PERSON_NUMBERTakes SSN + Date → returns Person Number (100045)
XXVA_GET_LATEST_ASSIGNMENT_NUMBERTakes SSN + Date → returns Assignment Number (E12345)

That’s the simple version. But there’s a performance problem.

The Problem: Same SSN, Three Rows, Three Identical DB Calls

One employee can have multiple rows in the vendor file — one per benefit plan. If an employee has 3 benefit plans (Dental, Medical, Vision), the file has 3 rows with the same SSN**. Without optimization, the formula calls the value set 3 times for the exact same SSN and gets the exact same answer 3 times.

Without caching — 3 rows, same SSN

Row 1 (DENTAL01): SSN 123-45-6789 → call DB → Person# 100045 [OK]** Row 2 (MEDICAL01):** SSN 123-45-6789 → call DB again → Person# 100045 ← same SSN, wasted call** Row 3 (VISION01):** SSN 123-45-6789 → call DB again → Person# 100045 ← same SSN, wasted call

The Fix: Cache with WSA

The formula uses WSA to remember the answer (explained in the WSA Deep Dive after Step 4). The logic is simple:

Did I already look up this SSN?

WSA_EXISTS(‘PER_123-45-6789_2024-01-15’)

YES → Read from cache

WSA_GET(‘PER_123-45-6789_2024-01-15’, ’ ’)**→ 100045. Done. No DB call.

NO → Call DB, then save to cache

GET_VALUE_SET(…) → 100045 WSA_SET(‘PER_123-45-6789_2024-01-15’, 100045)

With WSA caching — same 3 rows, same SSN

Row 1 (DENTAL01):** WSA_EXISTS? NO → call DB → 100045 → WSA_SET (save it) [OK]** Row 2 (MEDICAL01):** WSA_EXISTS? YES → WSA_GET → 100045. Zero DB calls. [OK]** Row 3 (VISION01):** WSA_EXISTS? YES → WSA_GET → 100045. Zero DB calls. [OK]

Here’s what the actual code looks like:

/* Build a unique WSA key from SSN + Date */
/* e.g. 'PER_123-45-6789_2024-01-15' */

IF WSA_EXISTS('PER_' || POSITION1 || '_' || POSITION2) THEN
(
    /* Cache hit — read stored values */
    L_PersonNumber     = WSA_GET('PER_' || POSITION1 || '_' || POSITION2, ' ')
    l_AssignmentNumber = WSA_GET('ASG_' || POSITION1 || '_' || POSITION2, ' ')
)
ELSE
(
    /* Cache miss — call value sets (hits DB) */
    l_AssignmentNumber = GET_VALUE_SET('XXVA_GET_LATEST_ASSIGNMENT_NUMBER', ...)
    L_PersonNumber     = GET_VALUE_SET('XXVA_GET_PERSON_NUMBER', ...)

    /* Save to WSA — next row with same SSN skips DB */
    WSA_SET('PER_' || POSITION1 || '_' || POSITION2, L_PersonNumber)
    WSA_SET('ASG_' || POSITION1 || '_' || POSITION2, l_AssignmentNumber)
)

After Step 3: L_PersonNumber = '100045' and l_AssignmentNumber = 'E12345'

Step 4: MultipleEntryCount

Person 100045 + Element Dental EE Deduction + Date 2024-01-15→ l_MultipleEntryCount = 1 (or 2, 3… if entries already exist)

What Is It?

When the same person has multiple entries of the same element in the same payroll period, Oracle needs a sequence number to tell them apart. That number is MultipleEntryCount.

When does this happen in the vendor interface? Each pay period, the vendor sends a new deduction file. If person 100045 already has a Dental EE Deduction entry from a previous load, and this batch sends another one (maybe a mid-period adjustment), the new entry needs a higher count.

If you know SQL, it’s this:

ROW_NUMBER() OVER (PARTITION BY person, element, payroll_period)  =  MultipleEntryCount

Here’s what it looks like in PAY_ELEMENT_ENTRIES_F after multiple loads:

Person#ElementEffectiveStartDateAmountMultipleEntryCountSource
100045Dental EE Deduction2024-01-15$150.001January batch
100045Dental EE Deduction2024-01-20$25.002Mid-period adjustment
100045Medical EE Deduction2024-01-15$200.001← different element, count resets to 1

The partition key is Person + Element + Payroll Period. Same person + same element = same group, count increments. Different element = new group, count resets to 1. If two entries in the same group get the same count, Oracle overwrites the first one — data is lost.

The Problem: Fast Formula Has No Memory

In PL/SQL, you’d do this in a loop. The counter variable lives across iterations:

-- PL/SQL: variable persists across loop iterations
l_counter := 0;
FOR rec IN cursor LOOP
    l_counter := l_counter + 1;
    -- Row 1: l_counter = 1
    -- Row 2: l_counter = 2  ← remembers what happened in Row 1
END LOOP;

Fast Formula is not a loop. The HDL engine calls the formula once per row as a separate, independent invocation. All local variables are destroyed after each call. It’s like calling a standalone function 10,000 times — each call starts from zero with no memory of the previous call.

So the formula has to ask the database: “What’s the highest count that already exists?” The value set runs something like this behind the scenes against PAY_ELEMENT_ENTRIES_F:

SELECT MAX(pee.MULTIPLE_ENTRY_COUNT)
FROM   PAY_ELEMENT_ENTRIES_F  pee
      ,PAY_ELEMENT_TYPES_F    pet
      ,PER_ALL_ASSIGNMENTS_M  paam
WHERE  pee.ELEMENT_TYPE_ID  = pet.ELEMENT_TYPE_ID
AND    pee.PERSON_ID         = paam.PERSON_ID
AND    pet.ELEMENT_NAME      = 'Dental EE Deduction'
AND    paam.PERSON_NUMBER    = '100045'
AND    '2024-10-15' BETWEEN pee.EFFECTIVE_START_DATE AND pee.EFFECTIVE_END_DATE

This works fine when each batch has only one row per person+element. But what if the batch has two?

The Bug: Two Rows Read the Same Stale MAX

Here’s what’s already in PAY_ELEMENT_ENTRIES_F from last month’s load:

ELEMENT_ENTRY_IDPERSON_IDELEMENT_TYPE_IDEFFECTIVE_START_DATEMULTIPLE_ENTRY_COUNTENTRY_TYPE
30000001234510004550001 (Dental EE Deduction)01-Oct-20241E

Now our vendor batch has two new Dental EE Deduction rows for the same person. The formula runs SELECT MAX(MULTIPLE_ENTRY_COUNT) for each — but the problem is Row 5’s INSERT hasn’t reached the table yet when Row 8 queries it:

Row 5 processes — formula queries the table:

SELECT MAX(MULTIPLE_ENTRY_COUNT) FROM PAY_ELEMENT_ENTRIES_F
WHERE PERSON_ID = 100045 AND ELEMENT_TYPE_ID = 50001  → Returns 1

Formula assigns: 1 + 1 = 2   ← this row is still in the HDL batch, NOT yet inserted into PAY_ELEMENT_ENTRIES_F

Row 8 processes — formula queries the SAME table:

SELECT MAX(MULTIPLE_ENTRY_COUNT) FROM PAY_ELEMENT_ENTRIES_F
WHERE PERSON_ID = 100045 AND ELEMENT_TYPE_ID = 50001  → STILL returns 1!

Formula assigns: 1 + 1 = 2   ← SAME count as Row 5!

What the generated .dat file looks like — both rows got the same count:

ElementEntry.dat — FAIL output

Existing entry (already in Oracle):

ElementNameDental EE Deduction
MultipleEntryCount1

Row 5 output ($175.00):

ElementNameDental EE Deduction
MultipleEntryCount2

Row 8 output ($200.00) — SAME count!

DARK_0

BUG: Two rows in the .dat file with MultipleEntryCount = 2. When Oracle loads this file, Row 8 overwrites Row 5. $175.00 entry is lost.

The Fix: WSA Tracks What the Table Can’t See Yet

WSA acts as an in-memory counter that survives between formula calls. Row 5 saves its assigned count to WSA. When Row 8 runs, it reads from WSA instead of querying the table:

RowWSA has data?Source of MAXAssignsSaves to WSA
Row 5NOPAY_ELEMENT_ENTRIES_F → MAX = 12WSA_SET(2)
Row 8YES → 2WSA memory (skips table)3WSA_SET(3)

What the .dat file looks like — each row gets a unique count:

ElementEntry.dat — PASS output

Existing entry (already in Oracle):

ElementNameDental EE Deduction
MultipleEntryCount1

Row 5 output ($175.00):

ElementNameDental EE Deduction
MultipleEntryCount2 [OK]

Row 8 output ($200.00):

DARK_1

PASS Three unique MultipleEntryCount values (1, 2, 3) in the .dat file. Oracle loads all three entries successfully.

The Fast Formula Code

/* Check: did a previous row already assign a count for this combo? */
IF WSA_EXISTS('MEC_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2) THEN
(
    /* YES — read last assigned count and add 1 */
    l_MultipleEntryCount = WSA_GET('MEC_' || ..., 0) + 1
)
ELSE
(
    /* NO — first row for this combo. Ask the database. */
    l_db_max = GET_VALUE_SET('MAX_MULTI_ENTRY_COUNT', ...)

    IF ISNULL(l_db_max) = 'N' THEN
        l_MultipleEntryCount = 1              /* Nothing in cloud → start at 1 */
    ELSE
        l_MultipleEntryCount = l_db_max + 1  /* Cloud has 1 → assign 2 */
)

/* Save what we assigned — next row reads this instead of hitting DB */
WSA_SET('MEC_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2, l_MultipleEntryCount)

Summary in one line: WSA is a working storage area that persists across formula invocations — like a PL/SQL package variable. The formula writes the assigned count to WSA, so the next row with the same combo reads from memory instead of hitting a stale database. The formula writes the assigned count to WSA, so the next row with the same combo reads from memory instead of hitting a stale database.

After Step 4: l_MultipleEntryCount = 2 (cloud had 1, so we assigned 1 + 1)

WSA in This Formula — Connecting Step 3 and Step 4

You’ve now seen WSA used twice in the MAP block, but for two completely different reasons. Let’s connect them before moving to Step 5.

StepWSA KeyWhat It StoresWhyWhat Breaks Without It
Step 3PER_**ASG_Person NumberAssignment NumberPerformanceSame SSN queried 3x instead of 1x. Slow but correct.
Step 4MEC_Last assigned MultipleEntryCountCorrectnessDuplicate MULTIPLE_ENTRY_COUNT in PAY_ELEMENT_ENTRIES_F. Data lost.

This is the key distinction:

Removes WSA from Step 3 → formula still works correctly**

It just runs slower (3 DB calls instead of 1 per SSN group)

Remove WSA from Step 4 → formula produces wrong output

Duplicate counts → rows overwrite each other in PAY_ELEMENT_ENTRIES_F

Both use the same WSA methods (WSA_EXISTS, WSA_GET, WSA_SET), same pattern (check → hit or miss → store), but different purposes. Step 3 is optional optimization. Step 4 is mandatory for data integrity.


You’ve now seen WSA used in Step 3 and Step 4. Let’s go deeper into how it works, what this formula caches, and one critical deployment rule you can’t skip.

What Is WSA?

WSA (Working Storage Area) is, per Oracle documentation, a mechanism for storing global values across formulas. Local variables die after each formula invocation, but WSA values persist across calls within the same session. You write a value on Row 1, and you can read it back on Row 500. WSA names are case-independent'PER_123' and 'per_123' refer to the same item.

In PL/SQL terms: WSA is a package-level associative array (TABLE OF VARCHAR2 INDEX BY VARCHAR2). It persists across function calls within the same session.

The API — Four Methods

MethodPL/SQL EquivalentWhat It Does
WSA_EXISTS(item [, type])g_cache.EXISTS(key)Tests whether item exists in the storage area. Optional type parameter restricts to a specific data type (TEXT, NUMBER, DATE, TEXT_TEXT, TEXT_NUMBER, etc.)
WSA_GET(item, default-value)l_val := g_cache(key)Retrieves the stored value. If item doesn’t exist, returns the default-value instead. The data type of default-value determines the expected data type.
WSA_SET(item, value)g_cache(key) := valSets the value for item. Any existing item of the same name is overwritten.
WSA_DELETE([item])g_cache.DELETE(key)Deletes item from storage. If no name specified, all storage area data is deleted. Not used in this vendor formula, but important for cleanup scenarios.

Key detail from Oracle docs: WSA_GET always requires a default-value parameter. The formula always calls WSA_EXISTS first and only calls WSA_GET when the item is known to exist — so the default is never actually used, but it must still be provided. The data type of the default tells the engine what data type to expect.

Every WSA usage in this formula follows the same pattern. You already saw it twice:

/* THE PATTERN — same in Step 3, Step 4, and everywhere else */

IF WSA_EXISTS(l_key) THEN            /* 1. Check memory */
    l_value = WSA_GET(l_key, ' ')    /* 2a. HIT  — read from memory (default never used) */
ELSE
    l_value = GET_VALUE_SET(...)      /* 2b. MISS — call the database */
    WSA_SET(l_key, l_value)          /* 3.  SAVE — store for next row */

Where You Already Saw This Pattern

Key:‘PER_123-45-6789_2024-01-15’
Stores:Person Number (100045)
DB call saved:GET_VALUE_SET(‘XXVA_GET_PERSON_NUMBER’)
Purpose:Performance — same SSN in 3 rows, only 1 DB call
Key:‘MEC_100045_Dental EE Deduction_2024-01-15’
Stores:Last assigned count (2, then 3, then 4…)
DB call saved:GET_VALUE_SET(‘MAX_MULTI_ENTRY_COUNT’)
Purpose:Correctness — prevents duplicate MULTIPLE_ENTRY_COUNT

All WSA Keys This Formula Uses

Steps 3 and 4 are the two main ones, but the formula caches more. Here’s the complete list:

WSA KeyStoresUsed InType
PER__Person NumberStep 3Performance
ASG__Assignment NumberStep 3Performance
MEC_Last assigned MultipleEntryCountStep 4Correctness
SSID_, SSO_, EEVID_, EEVO_SourceSystemId/Owner lookupsStep 5Performance
HDR_Flag: ElementEntry header already generatedSection 7Correctness

Pattern: Performance keys (PER_, ASG_, SSID_) can be removed and the formula still works — just slower. Correctness keys (MEC_, HDR_) cannot be removed — the formula produces wrong data without them.

Traced Example: 3 Benefit Plan Rows, Same Employee

Watch Step 3 and Step 4 WSA caching in action across three rows for SSN 123-45-6789:

Vendor Input File — 3 rows for the same employee (SSN 123-45-6789)

RowPOS1 (SSN)POS2 (Date)POS3 (Plan)POS4 (Type)POS5 (Amt)
Row 1123-45-67892024-01-15DENTAL01PRE150.00
Row 2123-45-67892024-01-15MEDICAL01PRE75.50
Row 3123-45-67892024-01-15VISION01PRE12.30

Same SSN, same date — but different benefit plans. This is typical: one employee enrolled in Dental + Medical + Vision.

Now let’s trace what happens when the formula processes each row:

STEP 3 Person & Assignment Lookup

WSA CheckWSA_EXISTS(‘PER_123-45-6789_2024-01-15’)MISS
ActionCall DB → Person# = 100045, Asg# = E12345
WSA SaveWSA_SET(‘PER_…’, 100045)   WSA_SET(‘ASG_…’, E12345)

STEP 4 MultipleEntryCount

WSA CheckWSA_EXISTS(‘MEC_100045_Dental EE Deduction_2024’)MISS
ActionCall DB → MAX = NULL (no existing entry)
ResultMultipleEntryCount = 1   → WSA_SET(‘MEC_…Dental…’, 1)

DB calls: 11 — all cache misses (first time seeing this SSN)

STEP 3 Person & Assignment Lookup

WSA CheckWSA_EXISTS(‘PER_123-45-6789_2024-01-15’)HIT!
ActionWSA_GET → Person# 100045, Asg# E12345 — zero DB calls

STEP 4 MultipleEntryCount — different element name = new WSA key

WSA CheckWSA_EXISTS(‘MEC_100045_Medical EE Deduction_2024’)MISS
ActionCall DB → MAX = NULL
ResultMultipleEntryCount = 1   → WSA_SET(‘MEC_…Medical…’, 1)

DB calls: 4 — Step 3 saved 2 calls (cache hit), Step 4 missed (different element)

STEP 3 Person & Assignment Lookup

WSA CheckWSA_EXISTS(‘PER_123-45-6789_2024-01-15’)HIT!
ActionWSA_GET → Person# 100045, Asg# E12345 — zero DB calls

STEP 4 MultipleEntryCount — yet another element = yet another WSA key

WSA CheckWSA_EXISTS(‘MEC_100045_Vision EE Deduction_2024’)MISS
ActionCall DB → MAX = NULL
ResultMultipleEntryCount = 1

DB calls: 4 — same pattern as Row 2

The Pattern:

Step 3 (Person lookup)Step 4 (MEC)
Row 1MISS — call DBMISS — call DB
Row 2HIT — zero DB callsMISS — different element
Row 3HIT — zero DB callsMISS — different element

Step 3 always hits after Row 1 (same SSN = same key). Step 4 always misses here because each row maps to a different element. Step 4 WSA becomes critical when the batch has multiple rows for the same person + same element.

Performance at Scale

For 10,000 vendor rows where employees average 3 benefit plans each:

value set calls (10K × 11 per row)

63% reduction — Step 3 caching saves ~7 calls per duplicate SSN

Critical Rule: Set Threads = 1

There’s one deployment rule for WSA that you absolutely cannot skip:

If “Load Data from File” runs with 4 threads, each thread gets its own independent WSA:

Step 3 breaks:Thread 1 caches Person# for SSN 123. Thread 2 gets a different row for the same SSN — but Thread 2’s WSA is empty. It calls the value set again. (Wastes performance, but data is still correct.)
Step 4 breaks:Thread 1 assigns MultipleEntryCount = 2 and saves to its WSA. Thread 2 gets another row for the same person+element — but Thread 2’s WSA is empty. It queries the DB, gets MAX = 1, assigns count = 2. Duplicate. Data lost.

The fix:

My Client Groups → Payroll → Payroll Process Configuration → Threads = 1

Set thread count to 1 before running “Load Data from File.” All rows process sequentially in one thread. WSA works as a true shared cache across every row.


Step 5: SourceSystemId Resolution

All resolved values from Steps 1–4→ l_SourceSystemId = ‘HDL_XXVA_E12345_EE_100045_Dental EE Deduction_20240115’

Oracle HDL uses SourceSystemId as the MERGE key. If an entry already exists in cloud, the formula reuses its SourceSystemId (so HDL updates it). If not, it constructs one:

/* For active employees — construct using PersonNumber */
'HDL_XXVA' || l_AssignmentNumber || '_EE_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2

/* For terminated employees (PersonNumber unavailable) — use SSN */
'HDL_XXVA' || l_AssignmentNumber || '_EE_' || POSITION1 || '_' || l_ElementName || '_' || POSITION2

After all five steps, the formula has everything it needs: Element Name, Assignment Number, Person Number, MultipleEntryCount, SourceSystemId, and the dollar amount. Now it generates the HDL output rows (Sections 7 and 8).


Vendor Input (what the formula receives):

POSITION1SSN123-45-6789
POSITION2Date2024-01-15
POSITION3Plan CodeDENTAL01
POSITION4Ded TypePRE
POSITION5Amount150.00
POSITION11Status(blank = Active)

↓ Formula transforms (Steps 1–5 + LINEREPEATNO=1) ↓

HDL .dat Output (ElementEntry):

BusinessOperationMERGE
FileDiscriminatorElementEntry
LegislativeDataGroupName570
EffectiveStartDate2024/01/15
ElementNameDental EE Deduction
AssignmentNumberE12345
CreatorTypeH
EntryTypeE
MultipleEntryCount1
SourceSystemOwnerHDL_XXVA
SourceSystemIdHDL_XXVA_E12345_EE_…

After the five MAP steps, the formula has all the values it needs. Now it generates the actual HDL output. Each vendor source row produces multiple HDL output rows — one ElementEntry header on pass 1, followed by one ElementEntryValue per input value on passes 2 through 7. LINEREPEATNO controls which one gets generated on each pass.

How LINEREPEAT Works

The HDL engine calls the formula once per source row with LINEREPEATNO = 1. If the formula returns LINEREPEAT = 'Y', the engine calls the formula again for the same row — this time with LINEREPEATNO = 2.

/* HDL engine processes one vendor source row: */

/* Pass 1: LINEREPEATNO = 1 → ElementEntry header */
Formula outputs →  MERGE|ElementEntry|...|Dental EE Deduction|...
Formula returns →  LINEREPEAT = 'Y'   ← call me again

/* Pass 2: LINEREPEATNO = 2 → EEV: Amount = 150.00 */
Formula outputs →  MERGE|ElementEntryValue|...|Amount|...|150.00
Formula returns →  LINEREPEAT = 'Y'   ← call me again (more input values)

/* Pass 3: LINEREPEATNO = 3 → EEV: Period Type = Monthly */
Formula outputs →  MERGE|ElementEntryValue|...|Period Type|...|Monthly
Formula returns →  LINEREPEAT = 'Y'   ← call me again

/* ... passes 4–6 for Loan Number, Total Owed, Percentage (if applicable) ... */

/* Pass 7: LINEREPEATNO = 7 → EEV: Deduction Amount (last pass) */
Formula outputs →  MERGE|ElementEntryValue|...|Deduction Amount|...
Formula returns →  LINEREPEAT = 'N'   ← done, move to next source row

One source row → multiple output rows (1 ElementEntry + up to 6 ElementEntryValues). The HDL engine groups all ElementEntry rows together and all ElementEntryValue rows together in the final .dat file, separated by their METADATA header rows.

The .dat Output Structure

The final .dat file has two blocks. Each block starts with a METADATA row that defines the columns, followed by the MERGE data rows:

Block 1 — ElementEntryValue (generated by LINEREPEATNO = 2–7)

A         B                C    D           E                  F               G               J                  K
METADATA  ElementEntryVal  LDG  EffStart    ElementName        AssignmentNum   InputValueName  MultipleEntryCount ScreenEntryValue
MERGE     ElementEntryVal  570  22-09-2019  Dental EE Deduct   123141402543    Amount          3                  150.00
MERGE     ElementEntryVal  222  22-09-2019  Dental EE Deduct   123141402554    Amount          6                  25.72
MERGE     ElementEntryVal  570  22-09-2019  Dental EE Deduct   123141402543    Amount          1                  150.00
...       more rows

Block 2 — ElementEntry (generated by LINEREPEATNO = 1)

A         B             C    D           E                  F               G           I          J
METADATA  ElementEntry  LDG  EffStart    ElementName        AssignmentNum   CreatorType EntryType  MultipleEntryCount
MERGE     ElementEntry  570  22-09-2019  Dental EE Deduct   123141402543    H           E          3
MERGE     ElementEntry  222  22-09-2019  Dental EE Deduct   123141402554    H           E          6
...       more rows

The key columns to notice: ElementEntry has CreatorType and EntryType but no dollar amount. ElementEntryValue has InputValueName (always “Amount”) and ScreenEntryValue (the actual dollar amount like 150.00). Both carry MultipleEntryCount from Step 4.

What LINEREPEATNO = 1 Generates

On the first pass, the formula checks POSITION11 (the STATUS column from the vendor file). This decides whether we’re creating a new entry or end-dating an existing one:

POSITION11ElementEntry row generatedLINEREPEAT
Blank (Active)MERGE|ElementEntry|570|22-09-2019|Dental EE Deduction|123141402543|H||E|1**
EffectiveStartDate = POSITION2. No EndDate. CreatorType = H. EntryType = E.‘Y’→ needs pass 2
C (Cancel)MERGE|ElementEntry|570|22-09-2019|Dental EE Deduction|123141402543|H|2019/09/22|E|1|...|Y
Fetches original StartDate from cloud. Sets EndDate = cancellation date. Appends ReplaceLastEffectiveEndDate = Y.‘N’→ no detail needed

How the Code Actually Writes the ElementEntry Row

The formula does not** use positional output variables like HDL_LINE1_N. Instead, it assigns values to named output variables that match the METADATA column names. Then an explicit RETURN statement tells the HDL engine which variables to pick up and in what order.

Here’s the Active path (POSITION11 is blank):

IF LINEREPEATNO = 1 THEN
(
    /* ======================================== */
    /* ACTIVE entry — create new ElementEntry   */
    /* ======================================== */

    FileName                    = 'ElementEntry'
    BusinessOperation           = 'MERGE'
    FileDiscriminator           = 'ElementEntry'
    LegislativeDataGroupName    = l_LegislativeDataGroupName
    AssignmentNumber            = l_AssignmentNumber
    ElementName                 = l_ElementName
    EffectiveStartDate          = TO_CHAR(TO_DATE(TRIM(POSITION2),'YYYY/MM/DD'),'YYYY/MM/DD')
    EntryType                   = l_entry_type
    CreatorType                 = l_CreatorType
    SourceSystemOwner           = l_SourceSystemOwner
    SourceSystemId              = l_SourceSystemId
    LINEREPEAT                  = 'Y'             /* ← call me again for ElementEntryValue */

    RETURN BusinessOperation, FileName, FileDiscriminator,
           CreatorType, EffectiveStartDate, ElementName,
           LegislativeDataGroupName, EntryType, AssignmentNumber,
           SourceSystemOwner, SourceSystemId,
           LINEREPEAT, LINEREPEATNO
)

How the RETURN works: The variable names in the RETURN statement must match the METADATA column names exactly. The HDL engine maps each returned variable to its corresponding METADATA position and writes the pipe-delimited row in that order. FileName and FileDiscriminator go to positions [1] and [2]. The rest map by name to the METADATA array you defined in Section 5.

For a Cancel row (POSITION11 = ‘C’), the formula fetches the original start date from the cloud, sets an end date, and returns LINEREPEAT = 'N' (no pass 2 needed — you don’t need an ElementEntryValue for a cancellation):

IF (TRIM(POSITION11) = 'C') THEN
(
    /* Fetch the original start date from cloud */
    l_Effective_Start_Date = GET_VALUE_SET('XXVA_GET_EE_START_DATE', ...)

    /* Same named variables, but with end date + replace flag */
    FileName                    = 'ElementEntry'
    BusinessOperation           = 'MERGE'
    FileDiscriminator           = 'ElementEntry'
    EffectiveStartDate          = TO_CHAR(TO_DATE(l_Effective_Start_Date,...),'YYYY/MM/DD')
    EffectiveEndDate            = TO_CHAR(TO_DATE(TRIM(POSITION2),...),'YYYY/MM/DD')
    ReplaceLastEffectiveEndDate = 'Y'
    LINEREPEAT                  = 'N'              /* ← no pass 2 for cancel */
    /* ...same other variables as Active... */

    RETURN BusinessOperation, FileName, FileDiscriminator,
           CreatorType, EffectiveStartDate, EffectiveEndDate,
           ElementName, LegislativeDataGroupName, EntryType,
           AssignmentNumber, SourceSystemOwner, SourceSystemId,
           ReplaceLastEffectiveEndDate,
           LINEREPEAT, LINEREPEATNO
)

Notice the Cancel RETURN includes EffectiveEndDate and ReplaceLastEffectiveEndDate — both absent from the Active RETURN.

Duplicate Header Prevention (WSA)

One person can have multiple vendor rows (Dental, Medical, Vision) that all map to different elements. Each element needs exactly one ElementEntry row. But if two vendor rows map to the same element, the formula must not generate a duplicate header. It checks WSA:

IF WSA_EXISTS('HDR_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2) THEN
(
    /* Header already generated for this combo — skip to pass 2 */
    LINEREPEAT = 'Y'
    RETURN
)
/* First time for this combo — generate header, then mark in WSA */
WSA_SET('HDR_' || ..., 1)

Watch out: ISNULL is inverted

The formula checks ISNULL(l_ElementName) = 'N' before generating anything. In Fast Formula, 'N' means the value IS null (not found). If the vendor code didn’t map to any element, the formula skips the row silently.


Same Vendor Input Row → multiple ElementEntryValue outputs (one per input value):

↓ Each pass loads a different InputValueName ↓

Pass 2 — ElementEntryValue (Amount):

InputValueNameAmount
ScreenEntryValue150.00
ElementEntryId(SSID)HDL_XXVA_E12345_EE_… (links to parent ElementEntry)

Pass 3 — ElementEntryValue (Period Type):

InputValueNamePeriod Type
ScreenEntryValueMonthly

Pass 6 — ElementEntryValue (Percentage):

InputValueNamePercentage
ScreenEntryValue5.5

Passes 4, 5, 7 skipped — PRE type doesn’t use Loan Number, Total Owed, or Deduction Amount. The formula returns LINEREPEAT = 'Y' with no output data on those passes.

Passes 2 through 7 each generate one ElementEntryValue row. Each pass loads a different input value. The deduction type (POSITION4) controls which passes produce output and which ones skip.

What LINEREPEATNO = 2 Generates

Each ElementEntryValue pass sets InputValueName to a different value and loads the corresponding data into ScreenEntryValue:

ColumnValueSource
LINEREPEATNOInputValueNameScreenEntryValue source
2Amountl_Amount (POSITION5) = 150.00
3Period Typel_PeriodType (POSITION6) = Monthly
4Loan NumberPOSITION8 — LOAN type only
5Total Owedl_TotalOwed — LOAN type only
6Percentagel_Percentage (POSITION7) — PRE/POST type only
7Deduction Amountl_DeductionAmount — CU type only

How the Code Actually Writes the ElementEntryValue Row

Each pass from 2 to 7 follows the same structure. The key difference is the skip logic: each pass checks POSITION4 (deduction type) to decide whether to generate output or just return LINEREPEAT = 'Y' with no data (effectively skipping to the next pass). Same pattern — named output variables + explicit RETURN. But now FileDiscriminator = 'ElementEntryValue' (not ‘ElementEntry’), and the RETURN includes InputValueName, ScreenEntryValue, and the parent link "ElementEntryId(SourceSystemId)".

ELSE IF (LINEREPEATNO = 2) THEN
(
    l_InputValueName = 'Amount'

    /* Look up ElementEntryValue SourceSystemId from cloud (or construct new one) */
    l_EEV_SourceSystemId = GET_VALUE_SET(
        'XXVA_GET_EEV_SOURCE_SYSTEM_ID', ...)
    l_EEV_SourceSystemOwner = GET_VALUE_SET(
        'XXVA_GET_EEV_SOURCE_SYSTEM_OWNER', ...)

    /* If no existing SSID found, construct a new one */
    IF ISNULL(l_EEV_SourceSystemId) = 'N' THEN
    (
        l_EEV_SourceSystemId = 'HDL_XXVA' || l_AssignmentNumber
            || '_EEV_' || L_PersonNumber
            || '_' || l_ElementName
            || '_' || l_InputValueName
            || '_' || TO_CHAR(TO_DATE(TRIM(POSITION2),...),'YYYYMMDD')
    )

    /* ============================================= */
    /* Set the output variables for ElementEntryValue */
    /* ============================================= */

    FileName                          = 'ElementEntry'        /* always ElementEntry */
    BusinessOperation                 = 'MERGE'
    FileDiscriminator                 = 'ElementEntryValue'   /* ← THIS is the key difference */
    LegislativeDataGroupName          = l_LegislativeDataGroupName
    AssignmentNumber                  = l_AssignmentNumber
    ElementName                       = l_ElementName
    EntryType                         = l_entry_type
    EffectiveStartDate                = TO_CHAR(...)
    "ElementEntryId(SourceSystemId)"  = l_SourceSystemId      /* ← links to parent ElementEntry */
    SourceSystemId                    = l_EEV_SourceSystemId  /* ← EEV's own SSID */
    SourceSystemOwner                 = l_EEV_SourceSystemOwner
    InputValueName                    = l_InputValueName      /* 'Amount' */
    ScreenEntryValue                  = To_Char(TO_NUM(TRIM(l_Amount)))
    LINEREPEAT                        = 'Y'                  /* more passes to come (pass 7 returns 'N') */

    RETURN BusinessOperation, FileName, FileDiscriminator,
           AssignmentNumber, EffectiveStartDate, ElementName,
           EntryType, LegislativeDataGroupName,
           "ElementEntryId(SourceSystemId)",
           InputValueName, ScreenEntryValue,
           SourceSystemOwner, SourceSystemId,
           LINEREPEAT, LINEREPEATNO
)

Three things to notice:

1. FileName is still 'ElementEntry' — NOT 'ElementEntryValue'. Only the FileDiscriminator changes to 'ElementEntryValue'. This is how HDL knows the row goes into the ElementEntryValue block of the .dat file.**

2.** "ElementEntryId(SourceSystemId)" is set to the ElementEntry’s SourceSystemId (l_SourceSystemId). This is the parent-child link. The variable name contains parentheses, so it must be double-quoted in the formula code.**

3.** The ElementEntryValue has its own SourceSystemId (l_EEV_SourceSystemId), different from the parent ElementEntry’s. The formula first tries to find an existing one from the cloud via value set. If not found (ISNULL = 'N'), it constructs one with the pattern: HDL_XXVA + AssignmentNumber + _EEV_ + PersonNumber + _ElementName + _InputValueName + _Date.

The Parent-Child Link

The ElementEntryValue row must reference its parent ElementEntry row. HDL uses SourceSystemId to link them:

ElementEntry (Pass 1)

SourceSystemId = HDL_XXVA_E12345_EE_…

ElementEntryValue (Pass 2)

ElementEntryId(SSID) = HDL_XXVA_E12345_EE_…

SourceSystemId = HDL_XXVA_E12345_EEV_…

The ElementEntryValue’s ElementEntryId(SourceSystemId) matches the ElementEntry’s SourceSystemId. This is how HDL knows which entry this value belongs to.

The RTRIM Trick for Clean Numbers

the vendor sends amounts like 150.00, but Oracle elements expect clean numbers. The formula strips trailing zeros:

RTRIM(RTRIM(TRIM(l_Amount), '0'), '.')

/* 150.00 → 150 | 75.50 → 75.5 | 12.30 → 12.3 | 150.00 → 150.00 */

The inner RTRIM strips trailing zeros. The outer RTRIM strips the decimal point if nothing is left after it.


Vendor CSV Row: 123-45-6789,2024-01-15,DENTAL01,150.00,,,

STEP 1: Type → ER, Amount → 150.00

STEP 2: Key → DENTAL01 → Dental EE Deduction

STEP 3: SSN → Person# 100045, Asg# E12345 (WSA)

STEP 4: MultipleEntryCount = 1

STEP 5: SourceSystemId constructed

LINEREPEATNO=1 → ElementEntry:

MERGE|ElementEntry|570|2019/09/22|Dental EE Deduction|123141402543|H||E|1

LINEREPEATNO=2 → ElementEntryValue (Amount):

MERGE|ElementEntryValue|570|2019/09/22|Dental EE Deduction|123141402543|Amount||E||1|150.00

If you’ve read this far, you can now explain — without looking at any code — how an HDL Transformation Formula works end-to-end. You know what each OPERATION does, why METADATA arrays define the .dat column headers, how the MAP block transforms source data in 5 steps, why WSA exists (performance + correctness), how LINEREPEATNO generates multiple output rows (1 ElementEntry + up to 6 ElementEntryValues) from one source row, and how named RETURN variables map to METADATA columns.

That’s the foundation. The concepts don’t change whether you’re building an vendor deduction interface, a benefits enrollment loader, or a payroll costing feed. Every HDL Transformation Formula follows this same structure.

Coming Next — Part 2: Code Walkthrough

Part 2 takes every concept from this post and shows you the actual Fast Formula code that implements it. Line by line, with the Notepad++ syntax highlighting you’ve been seeing in the code snippets here.

What Part 2 will cover:

Full INPUTS ARE blockEvery POSITION mapped to its vendor column, every DEFAULT FOR explained
GET_VALUE_SET callsThe exact parameter string construction with pipe delimiters, date conversions, and ISNULL checking
WSA implementationReal WSA_EXISTS / WSA_GET / WSA_SET code with key construction patterns
SourceSystemId logicThe full lookup-or-construct pattern for both ElementEntry and ElementEntryValue SourceSystemIds
ESS_LOG_WRITE debuggingAdding trace logs at each step so you can debug formula execution in real time
Cancel vs Active branchingThe complete IF POSITION11 = ‘C’ block with date fetching from cloud

Later — Part 3: Build Your Own

Part 3 is the implementation guide. You’ll build an HDL Transformation Formula from scratch — from creating the formula in Oracle Cloud, defining all 11 value sets, configuring the HDL integration, running test loads, reading ESS logs, and troubleshooting the errors you’ll hit in production.

After Part 3, you’ll have a working formula you can adapt for any inbound payroll interface — not just one vendor.

Series Roadmap

Part 1: Pure Concepts ← This post

Part 2: Code Walkthrough Coming soon

Part 3: Build Your Own Coming soon

Abhishek Mohanty

ELEMENT_ENTRY_ID  PERSON_ID  ELEMENT_TYPE_ID         EFFECTIVE_START_DATE  MULTIPLE_ENTRY_COUNT  ENTRY_TYPE
300000012345      100045     50001 (Dental EE Ded)   01-Oct-2024           1                     E

https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700;800&display=swap”);

@media (prefers-color-scheme: dark) { .hdl-blog { background: #12131A !important; color: #C8C9D4 !important; } .hdl-blog p, .hdl-blog li { color: #C8C9D4 !important; } .hdl-blog strong { color: #EAEBF0 !important; } .hdl-blog em { color: #C8C9D4 !important; } .hdl-blog code { background: #1E1F2B !important; color: #D4D5DE !important; } .hdl-blog hr { border-color: #2A2B38 !important; }

.hdl-blog [style*=“background:linear-gradient(135deg,#1B4965”] { background: linear-gradient(135deg,#0D2B3E,#081C2B) !important; box-shadow: 0 4px 20px rgba(0,0,0,0.5) !important; } .hdl-blog [style*=“font-size:17px”][style*=“font-weight:700”] { color: #EAEBF0 !important; } .hdl-blog [style*=“font-size:15px”][style*=“font-weight:700”][style*=“border-left”] { color: #D4D5DE !important; }

.hdl-blog [style*=“background:#fff”] { background: #1A1B26 !important; } .hdl-blog [style*=“background:#F5F3EF”] { background: #16171F !important; } .hdl-blog [style*=“background:#FDF5ED”] { background: #1C1812 !important; border-color: #3D3224 !important; } .hdl-blog [style*=“background:#FCF0F0”] { background: #1C1414 !important; } .hdl-blog [style*=“background:#EDE8E0”] { background: #1E1F2B !important; }

.hdl-blog [style*=“#DDD8D0”] { border-color: #2A2B38 !important; } .hdl-blog td, .hdl-blog th, .hdl-blog tr { border-color: #2A2B38 !important; }

.hdl-blog td { color: #C8C9D4 !important; } .hdl-blog th { color: #fff !important; } .hdl-blog td[style*=“font-weight:700”] { color: #EAEBF0 !important; } .hdl-blog td[style*=“font-weight:600”] { color: #D4D5DE !important; } .hdl-blog td[style*=“font-weight:800”] { color: #EAEBF0 !important; }

.hdl-blog [style*=“color:#D4622B”] { color: #E89060 !important; } .hdl-blog td[style*=“color:#D4622B”] { color: #E89060 !important; } .hdl-blog [style*=“background:#D4622B”], .hdl-blog [style*=“background:linear-gradient(135deg,#D4622B”] { color: #fff !important; }

.hdl-blog [style*=“color:#2D8B6F”] { color: #5CC4A0 !important; } .hdl-blog [style*=“background:#2D8B6F”] { background: #1A5C47 !important; }

.hdl-blog [style*=“color:#C13B3B”] { color: #F08080 !important; } .hdl-blog [style*=“background:#C13B3B”] { background: #8B2020 !important; } .hdl-blog [style*=“border”][style*=“#C13B3B”] { border-color: #5C1A1A !important; }

.hdl-blog [style*=“color:#8B8FA8”] { color: #6B6F88 !important; }

.hdl-blog pre { background: #0D0E14 !important; border-color: #1E1F2B !important; }

.hdl-blog [style*=“background:#1E1E1E”] { background: #0D0E14 !important; } .hdl-blog [style*=“background:#1E1E1E”] td[style*=“color:#8B8FA8”] { color: #6B6F88 !important; } .hdl-blog [style*=“background:#1E1E1E”] td[style*=“color:#B5CEA8”] { color: #B5CEA8 !important; } .hdl-blog [style*=“background:#1E1E1E”] td[style*=“color:#CE9178”] { color: #CE9178 !important; } .hdl-blog [style*=“background:#1E1E1E”] td[style*=“color:#2D8B6F”] { color: #5CC4A0 !important; } .hdl-blog [style*=“background:#1E1E1E”] td[style*=“color:#C13B3B”] { color: #F08080 !important; }

.hdl-blog [style*=“background:#1B4965”] { background: #0D2B3E !important; }

.hdl-blog [style*=“font-size:28px”][style*=“color:#C13B3B”] { color: #F08080 !important; } .hdl-blog [style*=“font-size:28px”][style*=“color:#D4622B”] { color: #E89060 !important; }

.hdl-blog [style*=“background:#eee”] { background: #2A2B38 !important; color: #8B8FA8 !important; }

.hdl-blog span[style*=“border-radius:10px”] { color: #fff !important; } .hdl-blog span[style*=“border-radius:12px”] { color: #fff !important; }

.hdl-blog [style*=“font-size:30px”][style*=“font-weight:800”] { color: #F0F1F5 !important; }

.hdl-blog td[style*=“font-family:monospace”] { color: #C8C9D4 !important; }

.hdl-blog [style*=“color:#1A1A2E”] { color: #EAEBF0 !important; } .hdl-blog [style*=“color:#3D3D5C”] { color: #C8C9D4 !important; } }

.hdl-blog table { min-width: 400px; }

.hdl-blog .block-table { font-size: 11px !important; } .hdl-blog .block-table td, .hdl-blog .block-table th { padding: 5px 6px !important; font-size: 11px !important; white-space: nowrap !important; } .hdl-blog .db-table { font-size: 11px !important; } .hdl-blog .db-table td, .hdl-blog .db-table th { padding: 5px 6px !important; font-size: 11px !important; white-space: nowrap !important; }

.hdl-blog div[style*=“overflow:hidden”] { overflow-x: auto !important; }

Vendor Deduction Interface | ElementEntry + ElementEntryValue

This is Part 1 of a 3-part series on HDL Transformation Formulas. This post covers the concepts end-to-end — what each section of the formula does and why. No code to copy-paste here. Just the understanding you need before writing a single line.

HDL Transformation Formula Series

Pure Concepts ← You are here

What each section of the formula does. INPUTS, OPERATION, METADATA, MAP (5 steps), WSA, LINEREPEATNO, RETURN. Zero code to memorize — just understanding.

Code Walkthrough Coming soon

The actual formula code, explained line-by-line. Value set definitions, WSA implementation, date conversions, ISNULL patterns, ESS_LOG_WRITE debugging. Moderate complexity — you’ll be able to read any HDL formula after this.

Build Your Own Coming soon

Full implementation guide. Setting up the formula in Oracle, creating the value sets, configuring the HDL integration, testing with real data, debugging production issues. Copy-paste ready.

Abhishek Mohanty

Before we go section by section, here’s what this formula does end to end:

Vendor CSV File**SSN, Date, Code, Amounts

HDL Transformation This Formula

ElementEntry .dat Header + Value rows

A third-party benefits administration vendor (BenAdmin) sends a CSV with deduction and employer contribution amounts. This formula transforms each row into Oracle HDL format — resolving SSNs to Assignment Numbers, mapping vendor codes to Oracle Element Names, managing MultipleEntryCount, and generating both ElementEntry (header) and ElementEntryValue (detail) rows.


The vendor manages employee benefit enrollments — medical, dental, vision, life insurance, FSA, HSA, loans. Every pay period, they send a flat CSV file with deduction details to load into Oracle as Element Entries.

The Raw Input File Layout

Each row in the vendor file maps to one set of delimited columns. The HDL engine reads these into POSITION variables:

ColumnPositionDescriptionExample
SSNPOSITION1Employee Social Security Number123-45-6789
EFFECTIVE_DATEPOSITION2Date the deduction applies (YYYY-MM-DD)2024-01-15
BENEFIT_PLAN_CODEPOSITION3Vendor’s internal code for the benefit planDENTAL01
DEDUCTION_TYPEPOSITION4Controls LINEREPEATNO branches and how many input values are loadedLOAN, PRE, POST, CU
AMOUNTPOSITION5Deduction amount (InputValueName = ‘Amount’)150.00
PERIOD_TYPEPOSITION6Period type for the deduction(varies)
PERCENTAGEPOSITION7Percentage for PRE/POST type deductions(blank or value)
LOAN_NUMBERPOSITION8Loan number (LOAN type only)(blank or value)
POSITION9–10POSITION9–10Reserved / additional fields(varies)
STATUSPOSITION11C = Cancel/End-date, blank = Active/New(blank)

Key point:** POSITION4 (Deduction Type) is the most important field after SSN and Date. It controls the formula’s branching logic — which LINEREPEATNO passes execute, which input values get loaded (Amount, Period Type, Percentage, Loan Number, Total Owed, Deduction Amount), and even whether the formula generates output on certain passes. A LOAN type deduction goes through 7 passes. A regular deduction goes through fewer.

How One Vendor Row Becomes Multiple Input Values

A single vendor row carries multiple amounts for the same deduction. The formula uses LINEREPEATNO to load each input value in a separate pass. For a LOAN type deduction, one source row generates up to 7 output rows:

/* One vendor row: */
123-45-6789,2024-01-15,DENTAL01,LOAN,150.00,Monthly,5.5,LN-001,,,,

/* Formula generates (up to 7 passes): */
Pass 1 (LINEREPEATNO=1): ElementEntry header
Pass 2 (LINEREPEATNO=2): ElementEntryValue → Amount = 150.00
Pass 3 (LINEREPEATNO=3): ElementEntryValue → Period Type = Monthly
Pass 4 (LINEREPEATNO=4): ElementEntryValue → Loan Number = LN-001
Pass 5 (LINEREPEATNO=5): ElementEntryValue → Total Owed = ...
Pass 6 (LINEREPEATNO=6): ElementEntryValue → Percentage = 5.5
Pass 7 (LINEREPEATNO=7): ElementEntryValue → Deduction Amount = ...

Not every deduction type needs all 7 passes. The formula checks POSITION4 on each pass — if the type doesn’t apply (e.g. Percentage only runs for PRE/POST types), it returns LINEREPEAT = 'Y' with no output, effectively skipping that pass.

Understanding MultipleEntryCount

Oracle HCM draws a fundamental distinction between recurring and non-recurring elements when it comes to MultipleEntryCount:

Monthly salary, standing allowance

MultipleEntryCount is not required as a key when using SourceSystemId.

“You don’t need to supply the MultipleEntryCount attribute as source keys to uniquely identify the records.” — Oracle Docs

Benefits deductions (our vendor elements)

MultipleEntryCount must be incremented for each entry of the same assignment + element within the same payroll period.

“You must increment the value of MultipleEntryCount for each entry of the same assignment and element.” — Oracle Docs

The vendor interface loads non-recurring elements that allow multiple entries. This means the formula must query the cloud for the current highest MultipleEntryCount before assigning the next one — and track assigned values across rows within the same batch using WSA.

Key Takeaway: Three benefit plan rows (Dental, Medical, Vision) for the same employee map to three different elements, so they each get independent entries with their own MultipleEntryCount. MultipleEntryCount is needed when the same non-recurring element requires multiple entries for the same assignment within the same payroll period.


The vendor file gives us an SSN and an Vendor Deduction Code. Oracle HCM needs an Assignment Number and an Oracle Element Name. These are completely different identifiers in completely different systems. Value Sets act as the bridge — SQL-backed lookup functions that run inside the Fast Formula engine.

VENDOR ENVIRONMENT

SSN: 123-45-6789

Code: DENTAL01

VALUE SETS

ORACLE WORLD

Asg#: E12345

Element: Dental EE Deduction

The formula uses 11 value sets. Here’s what each one does:

#Value SetWhat It DoesReturns
1XXVA_DEDUCTION_CODESMaps vendor plan code (DENTAL01) to Oracle Element NameElement Name
2XXVA_DEDUCTION_CODES_INPUTGets Input Value Name for the element (e.g. Amount)Input Value Name
3XXVA_GET_LATEST_ASSIGNMENT_NUMBERResolves SSN + date → Assignment NumberAssignment# (E12345)
4XXVA_GET_PERSON_NUMBERResolves SSN → Person NumberPerson# (100045)
5MAX_MULTI_ENTRY_COUNTGets highest existing MultipleEntryCount for Person+Element+DateMax count (or NULL)
6–7GET_ELEMENT_ENTRY_SOURCE_SYSTEM_ID / _OWNERRetrieves existing SourceSystemId/Owner for MERGE key reuseExisting SSID/SSO
8–9GET_ELEMENT_ENTRY_VALUE_SOURCE_SYSTEM_ID / _OWNERSame but at Element Entry Value levelElementEntryValue-level SSID/SSO
10GET_ELEMENT_ENTRY_START_DATEFor Cancel rows — gets original start dateOriginal start date
11GET_ELEMENT_ENTRY_INPUT_START_DATESame but at ElementEntryValue level (date-tracked scenarios)ElementEntryValue original start date

Translate vendor codes → Oracle element names. Called once per row regardless. No caching benefit.

Resolve SSN/Person data. Same SSN appears across multiple rows — WSA caching saves significant performance here.


INPUTS ARE OPERATION (TEXT),
LINEREPEATNO (NUMBER),
LINENO (NUMBER),
POSITION1 (TEXT), POSITION2 (TEXT), POSITION3 (TEXT),
POSITION4 (TEXT), POSITION5 (TEXT), POSITION6 (TEXT),
POSITION7 (TEXT), POSITION8 (TEXT),
POSITION9 (TEXT), POSITION10 (TEXT), POSITION11 (TEXT)

DEFAULT FOR LINENO IS 1
DEFAULT FOR LINEREPEATNO IS 1
DEFAULT FOR POSITION1 IS 'NO DATA'
DEFAULT FOR POSITION2 IS 'NO DATA'
/* ... same for POSITION3 through POSITION11 ... */
VariableWhat It Does
OPERATIONThe HDL engine calls the formula multiple times with different values: FILETYPE, DELIMITER, READ, NUMBEROFBUSINESSOBJECTS, METADATALINEINFORMATION, then MAP per row. The formula is a router.
LINEREPEATNOThe repeat counter. When formula sets LINEREPEAT = 'Y', HDL re-invokes for the same row with incremented LINEREPEATNO. One input row can generate up to 7 HDL output rows: 1 ElementEntry header (pass 1) + up to 6 ElementEntryValue rows (passes 2–7), one per input value (Amount, Period Type, Loan Number, Total Owed, Percentage, Deduction Amount). The deduction type (POSITION4) controls how many passes run.
LINENOLine number from the source file (1-based). Useful for error tracing.
POSITION1–11Map directly to CSV columns in order. HDL engine splits each line by delimiter and populates these.

Why DEFAULT FOR is required: When HDL calls the formula for non-MAP operations (like FILETYPE), the POSITION variables aren’t populated — no source row is being processed. Without defaults, the formula throws a null reference error at runtime.


InputHDL engine asks: “What file type? What delimiter? How many objects?”
Formula returnsDELIMITED, comma, NONE, 2
HDL outputNothing written to .dat yet — engine is just being configured
IF OPERATION = 'FILETYPE' THEN
    OUTPUTVALUE = 'DELIMITED'
ELSE IF OPERATION = 'DELIMITER' THEN
    OUTPUTVALUE = ','
ELSE IF OPERATION = 'READ' THEN
    OUTPUTVALUE = 'NONE'
ELSE IF OPERATION = 'NUMBEROFBUSINESSOBJECTS' THEN
(
    OUTPUTVALUE = '2'
    RETURN OUTPUTVALUE
)

FILETYPE**DELIMITED

DELIMITER ,

READ NONE

OBJECTS 2

METADATA

MAP (per row)

OperationEngine AsksOur AnswerWhy
FILETYPE”What kind of file?”DELIMITEDOnly valid option for HDL transformation
DELIMITER”What separates values?“,the vendor sends CSV. Default is pipe (|), so we override.
READ”Skip header rows?”NONEvendor file has no header row — process every line.
NUMBEROF…”How many HDL objects?“2ElementEntry (header) + ElementEntryValue (detail with amount)

InputHDL engine asks: “What columns does each object have?”
Formula returnsMETADATA1[ ] array, METADATA2[ ] array
HDL writes to .datMETADATA|ElementEntry|LegislativeDataGroupName|EffectiveStartDate|…METADATA|ElementEntryValue|LegislativeDataGroupName|EffectiveStartDate|…

After the setup handshake, the HDL engine calls the formula with OPERATION = 'METADATALINEINFORMATION'. This is where the formula defines the column headers** for the .dat output file. These become the METADATA rows you see at the top of each block in the .dat file.

The .dat File Has Two METADATA Header Rows

Since we told the engine NUMBEROFBUSINESSOBJECTS = 2 (ElementEntry + ElementEntryValue), the formula must define two METADATA arrays — one per object. These become the two header rows in the .dat file:

/* This header row in the .dat file: */
METADATA|ElementEntry|LegislativeDataGroupName|EffectiveStartDate|ElementName|AssignmentNumber|CreatorType|EffectiveEndDate|EntryType|MultipleEntryCount

/* Is generated by this code: */

The Code — METADATA1 (ElementEntry Header)

The formula uses an array variable called METADATA1. Each array position maps to a column in the .dat header. Positions [1] and [2] are reserved by the HDL engine for FileName and FileDiscriminator — the formula starts filling from position [3].

ELSE IF OPERATION = 'METADATALINEINFORMATION' THEN
(
    /* ================================================= */
    /* METADATA1 — ElementEntry column definitions        */
    /* [1] = FileName (auto-filled by HDL engine)         */
    /* [2] = FileDiscriminator (auto-filled by HDL engine)*/
    /* [3] onwards = we define                            */
    /* ================================================= */

    METADATA1[3]  = 'LegislativeDataGroupName'
    METADATA1[4]  = 'EffectiveStartDate'
    METADATA1[5]  = 'ElementName'
    METADATA1[6]  = 'AssignmentNumber'
    METADATA1[7]  = 'CreatorType'
    METADATA1[8]  = 'EffectiveEndDate'
    METADATA1[9]  = 'EntryType'
    METADATA1[10] = 'MultipleEntryCount'
    METADATA1[11] = 'SourceSystemOwner'
    METADATA1[12] = 'SourceSystemId'
    METADATA1[13] = 'ReplaceLastEffectiveEndDate'

The HDL engine reads this array and writes the following row to the .dat file:

METADATA|ElementEntry|LegislativeDataGroupName|EffectiveStartDate|ElementName|AssignmentNumber|CreatorType|EffectiveEndDate|EntryType|MultipleEntryCount|SourceSystemOwner|SourceSystemId|ReplaceLastEffectiveEndDate

The Code — METADATA2 (ElementEntryValue Header)

Same pattern. METADATA2 array defines the columns for the ElementEntryValue block:

    /* ================================================= */
    /* METADATA2 — ElementEntryValue column definitions   */
    /* ================================================= */

    METADATA2[3]  = 'LegislativeDataGroupName'
    METADATA2[4]  = 'EffectiveStartDate'
    METADATA2[5]  = 'ElementName'
    METADATA2[6]  = 'AssignmentNumber'
    METADATA2[7]  = 'InputValueName'              /* ← changes per pass */
    METADATA2[8]  = 'EffectiveEndDate'
    METADATA2[9]  = 'EntryType'
    METADATA2[10] = 'MultipleEntryCount'
    METADATA2[11] = 'ScreenEntryValue'            /* ← the actual value */
    METADATA2[12] = '"ElementEntryId(SourceSystemId)"'  /* parent link */
    METADATA2[13] = 'SourceSystemOwner'
    METADATA2[14] = 'SourceSystemId'
    METADATA2[15] = 'ReplaceLastEffectiveEndDate'

    RETURN METADATA1, METADATA2
)

This generates the second header row in the .dat file:

METADATA|ElementEntryValue|LegislativeDataGroupName|EffectiveStartDate|ElementName|AssignmentNumber|InputValueName|EffectiveEndDate|EntryType|MultipleEntryCount|ScreenEntryValue|ElementEntryId(SSID)|SourceSystemOwner|SourceSystemId|ReplaceLastEffectiveEndDate

How METADATA Links to RETURN in Sections 7 and 8

The column names in the METADATA arrays directly map to the named output variables in the formula’s RETURN statement. Here’s the connection:

/* METADATA defines the column header: */
METADATA1[5] = 'ElementName'

/* In the MAP block, the formula assigns the named variable: */
ElementName  = l_ElementName              /* = 'Dental EE Deduction' */

/* And includes it in the RETURN: */
RETURN ..., ElementName, ...

/* Result in .dat file:                                                */
/* METADATA |ElementEntry|...|ElementName                    |...      */
/* MERGE    |ElementEntry|...|Dental EE Deduction   |...      */
/*                             ↑ matched by variable name             */

The mapping rule:

METADATA1[N] defines column N header name for object 1 (ElementEntry)** In the MAP block, you assign a variable with that exact same name** and include it in the RETURN statement**

The HDL engine matches the RETURN variable name to the METADATA column name and writes the value into the correct position in the .dat file. The FileDiscriminator value ('ElementEntry' vs 'ElementEntryValue') tells the engine which METADATA block to use.


The reference vendor row used in all examples below:

Vendor Input Row

POSITION1SSN123-45-6789
POSITION2Effective Date2024-01-15
POSITION3Benefit PlanDENTAL01
POSITION4Deduction TypePRE
POSITION5Amount150.00
POSITION6Period TypeMonthly
POSITION7Percentage5.5
POSITION8Loan NumberLN-001
POSITION11Status(blank = Active)

This is the heart of the formula. When the HDL engine reaches a source row, it calls OPERATION = 'MAP'. The formula receives the raw CSV data in POSITION1–11 and must return Oracle HDL attributes. Five steps run in sequence.

Here’s what the formula needs to figure out for each row:

QuestionVendor Gives UsOracle NeedsStep
What type of deduction?POSITION4 (Deduction Type)PRE / POST / LOAN / CUStep 1
Which Oracle Element?DENTAL01Dental EE DeductionStep 2
Which employee?123-45-6789 (SSN)E12345 (Assignment#)Step 3
How many entries already exist?(doesn’t know)MultipleEntryCount = 2Step 4
New or existing entry?(doesn’t know)SourceSystemId for MERGEStep 5

STEP 1 Element Type

STEP 2 Element Lookup

STEP 3 Person / Assignment

STEP 4 MultipleEntryCount

STEP 5 SourceSystemId

Step 1: Read Input Values from POSITION Fields

POSITION4 = PRE→ l_DeductionType = ‘PRE’
POSITION5 = 150.00→ l_Amount = ‘150.00’
POSITION6 = Monthly→ l_PeriodType = ‘Monthly’
POSITION7 = 5.5→ l_Percentage = ‘5.5’
POSITION8 = LN-001→ l_LoanNumber = ‘LN-001’

The formula reads the deduction type from POSITION4 and the amount from POSITION5. It also captures other input values (Period Type, Percentage, Loan Number) from their respective positions for later LINEREPEATNO passes:

/* Read the key fields from the vendor row */
l_DeductionType    = TRIM(POSITION4)     /* 'PRE', 'POST', 'LOAN', 'CU' */
l_Amount           = TRIM(POSITION5)     /* '150.00' */
l_PeriodType       = TRIM(POSITION6)     /* 'Monthly' */
l_Percentage       = TRIM(POSITION7)     /* '5.5' (PRE/POST only) */
l_LoanNumber       = TRIM(POSITION8)     /* 'LN-001' (LOAN only) */

After this step: l_DeductionType = 'PRE' and l_Amount = '150.00'

Step 2: Resolve Element Name from Benefit Plan Code

POSITION3 = DENTAL01→ Value Set lookup
→ l_ElementName = ‘Dental EE Deduction’
→ l_InputValueName = ‘Amount’

The vendor uses its own benefit plan codes (DENTAL01, MEDICAL01, VISION01). Oracle doesn’t know these codes. The formula passes the vendor code to two value sets that translate it into Oracle terms:

/* Step 2: Translate vendor plan code → Oracle Element Name */

L_VendorPayCode = TRIM(POSITION3)
/* e.g. 'DENTAL01' */

/* Value set 1: vendor code → Oracle Element Name */
l_ElementName = GET_VALUE_SET('XXVA_DEDUCTION_CODES',
    '|=P_PAY_CODE=''' || L_VendorPayCode || '''')
/* 'DENTAL01' → 'Dental EE Deduction' */

/* Value set 2: vendor code → Input Value Name */
l_InputValueName = INITCAP(GET_VALUE_SET('XXVA_DEDUCTION_CODES_INPUT',
    '|=P_PAY_CODE=''' || L_VendorPayCode || ''''))
/* 'DENTAL01' → 'Amount' */

These are code-based** lookups — the value set definition maps each vendor code to its Oracle element. No person data is involved, so no WSA caching is needed here.

Value Set Translation

Vendor Code (POSITION3)Oracle Element NameInput Value Name
DENTAL01Dental EE DeductionAmount
MEDICAL01Medical EE DeductionAmount
VISION01Vision EE DeductionAmount

This mapping is defined in the value set configuration — not in the formula code. Adding a new benefit plan just means adding a row to the value set.

After Step 2: l_ElementName = 'Dental EE Deduction' and l_InputValueName = 'Amount'

Step 3: Resolve Person & Assignment

POSITION1 = 123-45-6789→ GET_VALUE_SET → L_PersonNumber = ‘100045’
POSITION2 = 2024-01-15→ GET_VALUE_SET → l_AssignmentNumber = ‘E12345’

Oracle HDL doesn’t understand SSN. It needs two things: Person Number and Assignment Number. Step 3 translates one into the other.

VENDOR FILE GIVES US

123-45-6789

SSN (POSITION1)

Value Set**calls DB

ORACLE HDL NEEDS

Person# 100045

Assignment# E12345

Two value sets do this translation:

XXVA_GET_PERSON_NUMBERTakes SSN + Date → returns Person Number (100045)
XXVA_GET_LATEST_ASSIGNMENT_NUMBERTakes SSN + Date → returns Assignment Number (E12345)

That’s the simple version. But there’s a performance problem.

The Problem: Same SSN, Three Rows, Three Identical DB Calls

One employee can have multiple rows in the vendor file — one per benefit plan. If an employee has 3 benefit plans (Dental, Medical, Vision), the file has 3 rows with the same SSN**. Without optimization, the formula calls the value set 3 times for the exact same SSN and gets the exact same answer 3 times.

Without caching — 3 rows, same SSN

Row 1 (DENTAL01): SSN 123-45-6789 → call DB → Person# 100045 [OK]** Row 2 (MEDICAL01):** SSN 123-45-6789 → call DB again → Person# 100045 ← same SSN, wasted call** Row 3 (VISION01):** SSN 123-45-6789 → call DB again → Person# 100045 ← same SSN, wasted call

The Fix: Cache with WSA

The formula uses WSA to remember the answer (explained in the WSA Deep Dive after Step 4). The logic is simple:

Did I already look up this SSN?

WSA_EXISTS(‘PER_123-45-6789_2024-01-15’)

YES → Read from cache

WSA_GET(‘PER_123-45-6789_2024-01-15’, ’ ’)**→ 100045. Done. No DB call.

NO → Call DB, then save to cache

GET_VALUE_SET(…) → 100045 WSA_SET(‘PER_123-45-6789_2024-01-15’, 100045)

With WSA caching — same 3 rows, same SSN

Row 1 (DENTAL01):** WSA_EXISTS? NO → call DB → 100045 → WSA_SET (save it) [OK]** Row 2 (MEDICAL01):** WSA_EXISTS? YES → WSA_GET → 100045. Zero DB calls. [OK]** Row 3 (VISION01):** WSA_EXISTS? YES → WSA_GET → 100045. Zero DB calls. [OK]

Here’s what the actual code looks like:

/* Build a unique WSA key from SSN + Date */
/* e.g. 'PER_123-45-6789_2024-01-15' */

IF WSA_EXISTS('PER_' || POSITION1 || '_' || POSITION2) THEN
(
    /* Cache hit — read stored values */
    L_PersonNumber     = WSA_GET('PER_' || POSITION1 || '_' || POSITION2, ' ')
    l_AssignmentNumber = WSA_GET('ASG_' || POSITION1 || '_' || POSITION2, ' ')
)
ELSE
(
    /* Cache miss — call value sets (hits DB) */
    l_AssignmentNumber = GET_VALUE_SET('XXVA_GET_LATEST_ASSIGNMENT_NUMBER', ...)
    L_PersonNumber     = GET_VALUE_SET('XXVA_GET_PERSON_NUMBER', ...)

    /* Save to WSA — next row with same SSN skips DB */
    WSA_SET('PER_' || POSITION1 || '_' || POSITION2, L_PersonNumber)
    WSA_SET('ASG_' || POSITION1 || '_' || POSITION2, l_AssignmentNumber)
)

After Step 3: L_PersonNumber = '100045' and l_AssignmentNumber = 'E12345'

Step 4: MultipleEntryCount

Person 100045 + Element Dental EE Deduction + Date 2024-01-15→ l_MultipleEntryCount = 1 (or 2, 3… if entries already exist)

What Is It?

When the same person has multiple entries of the same element in the same payroll period, Oracle needs a sequence number to tell them apart. That number is MultipleEntryCount.

When does this happen in the vendor interface? Each pay period, the vendor sends a new deduction file. If person 100045 already has a Dental EE Deduction entry from a previous load, and this batch sends another one (maybe a mid-period adjustment), the new entry needs a higher count.

If you know SQL, it’s this:

ROW_NUMBER() OVER (PARTITION BY person, element, payroll_period)  =  MultipleEntryCount

Here’s what it looks like in PAY_ELEMENT_ENTRIES_F after multiple loads:

Person#ElementEffectiveStartDateAmountMultipleEntryCountSource
100045Dental EE Deduction2024-01-15$150.001January batch
100045Dental EE Deduction2024-01-20$25.002Mid-period adjustment
100045Medical EE Deduction2024-01-15$200.001← different element, count resets to 1

The partition key is Person + Element + Payroll Period. Same person + same element = same group, count increments. Different element = new group, count resets to 1. If two entries in the same group get the same count, Oracle overwrites the first one — data is lost.

The Problem: Fast Formula Has No Memory

In PL/SQL, you’d do this in a loop. The counter variable lives across iterations:

-- PL/SQL: variable persists across loop iterations
l_counter := 0;
FOR rec IN cursor LOOP
    l_counter := l_counter + 1;
    -- Row 1: l_counter = 1
    -- Row 2: l_counter = 2  ← remembers what happened in Row 1
END LOOP;

Fast Formula is not a loop. The HDL engine calls the formula once per row as a separate, independent invocation. All local variables are destroyed after each call. It’s like calling a standalone function 10,000 times — each call starts from zero with no memory of the previous call.

So the formula has to ask the database: “What’s the highest count that already exists?” The value set runs something like this behind the scenes against PAY_ELEMENT_ENTRIES_F:

SELECT MAX(pee.MULTIPLE_ENTRY_COUNT)
FROM   PAY_ELEMENT_ENTRIES_F  pee
      ,PAY_ELEMENT_TYPES_F    pet
      ,PER_ALL_ASSIGNMENTS_M  paam
WHERE  pee.ELEMENT_TYPE_ID  = pet.ELEMENT_TYPE_ID
AND    pee.PERSON_ID         = paam.PERSON_ID
AND    pet.ELEMENT_NAME      = 'Dental EE Deduction'
AND    paam.PERSON_NUMBER    = '100045'
AND    '2024-10-15' BETWEEN pee.EFFECTIVE_START_DATE AND pee.EFFECTIVE_END_DATE

This works fine when each batch has only one row per person+element. But what if the batch has two?

The Bug: Two Rows Read the Same Stale MAX

Here’s what’s already in PAY_ELEMENT_ENTRIES_F from last month’s load:

ELEMENT_ENTRY_IDPERSON_IDELEMENT_TYPE_IDEFFECTIVE_START_DATEMULTIPLE_ENTRY_COUNTENTRY_TYPE
30000001234510004550001 (Dental EE Deduction)01-Oct-20241E

Now our vendor batch has two new Dental EE Deduction rows for the same person. The formula runs SELECT MAX(MULTIPLE_ENTRY_COUNT) for each — but the problem is Row 5’s INSERT hasn’t reached the table yet when Row 8 queries it:

Row 5 processes — formula queries the table:

SELECT MAX(MULTIPLE_ENTRY_COUNT) FROM PAY_ELEMENT_ENTRIES_F
WHERE PERSON_ID = 100045 AND ELEMENT_TYPE_ID = 50001  → Returns 1

Formula assigns: 1 + 1 = 2   ← this row is still in the HDL batch, NOT yet inserted into PAY_ELEMENT_ENTRIES_F

Row 8 processes — formula queries the SAME table:

SELECT MAX(MULTIPLE_ENTRY_COUNT) FROM PAY_ELEMENT_ENTRIES_F
WHERE PERSON_ID = 100045 AND ELEMENT_TYPE_ID = 50001  → STILL returns 1!

Formula assigns: 1 + 1 = 2   ← SAME count as Row 5!

What the generated .dat file looks like — both rows got the same count:

ElementEntry.dat — FAIL output

Existing entry (already in Oracle):

ElementNameDental EE Deduction
MultipleEntryCount1

Row 5 output ($175.00):

ElementNameDental EE Deduction
MultipleEntryCount2

Row 8 output ($200.00) — SAME count!

DARK_0

BUG: Two rows in the .dat file with MultipleEntryCount = 2. When Oracle loads this file, Row 8 overwrites Row 5. $175.00 entry is lost.

The Fix: WSA Tracks What the Table Can’t See Yet

WSA acts as an in-memory counter that survives between formula calls. Row 5 saves its assigned count to WSA. When Row 8 runs, it reads from WSA instead of querying the table:

RowWSA has data?Source of MAXAssignsSaves to WSA
Row 5NOPAY_ELEMENT_ENTRIES_F → MAX = 12WSA_SET(2)
Row 8YES → 2WSA memory (skips table)3WSA_SET(3)

What the .dat file looks like — each row gets a unique count:

ElementEntry.dat — PASS output

Existing entry (already in Oracle):

ElementNameDental EE Deduction
MultipleEntryCount1

Row 5 output ($175.00):

ElementNameDental EE Deduction
MultipleEntryCount2 [OK]

Row 8 output ($200.00):

DARK_1

PASS Three unique MultipleEntryCount values (1, 2, 3) in the .dat file. Oracle loads all three entries successfully.

The Fast Formula Code

/* Check: did a previous row already assign a count for this combo? */
IF WSA_EXISTS('MEC_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2) THEN
(
    /* YES — read last assigned count and add 1 */
    l_MultipleEntryCount = WSA_GET('MEC_' || ..., 0) + 1
)
ELSE
(
    /* NO — first row for this combo. Ask the database. */
    l_db_max = GET_VALUE_SET('MAX_MULTI_ENTRY_COUNT', ...)

    IF ISNULL(l_db_max) = 'N' THEN
        l_MultipleEntryCount = 1              /* Nothing in cloud → start at 1 */
    ELSE
        l_MultipleEntryCount = l_db_max + 1  /* Cloud has 1 → assign 2 */
)

/* Save what we assigned — next row reads this instead of hitting DB */
WSA_SET('MEC_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2, l_MultipleEntryCount)

Summary in one line: WSA is a working storage area that persists across formula invocations — like a PL/SQL package variable. The formula writes the assigned count to WSA, so the next row with the same combo reads from memory instead of hitting a stale database. The formula writes the assigned count to WSA, so the next row with the same combo reads from memory instead of hitting a stale database.

After Step 4: l_MultipleEntryCount = 2 (cloud had 1, so we assigned 1 + 1)

WSA in This Formula — Connecting Step 3 and Step 4

You’ve now seen WSA used twice in the MAP block, but for two completely different reasons. Let’s connect them before moving to Step 5.

StepWSA KeyWhat It StoresWhyWhat Breaks Without It
Step 3PER_**ASG_Person NumberAssignment NumberPerformanceSame SSN queried 3x instead of 1x. Slow but correct.
Step 4MEC_Last assigned MultipleEntryCountCorrectnessDuplicate MULTIPLE_ENTRY_COUNT in PAY_ELEMENT_ENTRIES_F. Data lost.

This is the key distinction:

Removes WSA from Step 3 → formula still works correctly**

It just runs slower (3 DB calls instead of 1 per SSN group)

Remove WSA from Step 4 → formula produces wrong output

Duplicate counts → rows overwrite each other in PAY_ELEMENT_ENTRIES_F

Both use the same WSA methods (WSA_EXISTS, WSA_GET, WSA_SET), same pattern (check → hit or miss → store), but different purposes. Step 3 is optional optimization. Step 4 is mandatory for data integrity.


You’ve now seen WSA used in Step 3 and Step 4. Let’s go deeper into how it works, what this formula caches, and one critical deployment rule you can’t skip.

What Is WSA?

WSA (Working Storage Area) is, per Oracle documentation, a mechanism for storing global values across formulas. Local variables die after each formula invocation, but WSA values persist across calls within the same session. You write a value on Row 1, and you can read it back on Row 500. WSA names are case-independent'PER_123' and 'per_123' refer to the same item.

In PL/SQL terms: WSA is a package-level associative array (TABLE OF VARCHAR2 INDEX BY VARCHAR2). It persists across function calls within the same session.

The API — Four Methods

MethodPL/SQL EquivalentWhat It Does
WSA_EXISTS(item [, type])g_cache.EXISTS(key)Tests whether item exists in the storage area. Optional type parameter restricts to a specific data type (TEXT, NUMBER, DATE, TEXT_TEXT, TEXT_NUMBER, etc.)
WSA_GET(item, default-value)l_val := g_cache(key)Retrieves the stored value. If item doesn’t exist, returns the default-value instead. The data type of default-value determines the expected data type.
WSA_SET(item, value)g_cache(key) := valSets the value for item. Any existing item of the same name is overwritten.
WSA_DELETE([item])g_cache.DELETE(key)Deletes item from storage. If no name specified, all storage area data is deleted. Not used in this vendor formula, but important for cleanup scenarios.

Key detail from Oracle docs: WSA_GET always requires a default-value parameter. The formula always calls WSA_EXISTS first and only calls WSA_GET when the item is known to exist — so the default is never actually used, but it must still be provided. The data type of the default tells the engine what data type to expect.

Every WSA usage in this formula follows the same pattern. You already saw it twice:

/* THE PATTERN — same in Step 3, Step 4, and everywhere else */

IF WSA_EXISTS(l_key) THEN            /* 1. Check memory */
    l_value = WSA_GET(l_key, ' ')    /* 2a. HIT  — read from memory (default never used) */
ELSE
    l_value = GET_VALUE_SET(...)      /* 2b. MISS — call the database */
    WSA_SET(l_key, l_value)          /* 3.  SAVE — store for next row */

Where You Already Saw This Pattern

Key:‘PER_123-45-6789_2024-01-15’
Stores:Person Number (100045)
DB call saved:GET_VALUE_SET(‘XXVA_GET_PERSON_NUMBER’)
Purpose:Performance — same SSN in 3 rows, only 1 DB call
Key:‘MEC_100045_Dental EE Deduction_2024-01-15’
Stores:Last assigned count (2, then 3, then 4…)
DB call saved:GET_VALUE_SET(‘MAX_MULTI_ENTRY_COUNT’)
Purpose:Correctness — prevents duplicate MULTIPLE_ENTRY_COUNT

All WSA Keys This Formula Uses

Steps 3 and 4 are the two main ones, but the formula caches more. Here’s the complete list:

WSA KeyStoresUsed InType
PER__Person NumberStep 3Performance
ASG__Assignment NumberStep 3Performance
MEC_Last assigned MultipleEntryCountStep 4Correctness
SSID_, SSO_, EEVID_, EEVO_SourceSystemId/Owner lookupsStep 5Performance
HDR_Flag: ElementEntry header already generatedSection 7Correctness

Pattern: Performance keys (PER_, ASG_, SSID_) can be removed and the formula still works — just slower. Correctness keys (MEC_, HDR_) cannot be removed — the formula produces wrong data without them.

Traced Example: 3 Benefit Plan Rows, Same Employee

Watch Step 3 and Step 4 WSA caching in action across three rows for SSN 123-45-6789:

Vendor Input File — 3 rows for the same employee (SSN 123-45-6789)

RowPOS1 (SSN)POS2 (Date)POS3 (Plan)POS4 (Type)POS5 (Amt)
Row 1123-45-67892024-01-15DENTAL01PRE150.00
Row 2123-45-67892024-01-15MEDICAL01PRE75.50
Row 3123-45-67892024-01-15VISION01PRE12.30

Same SSN, same date — but different benefit plans. This is typical: one employee enrolled in Dental + Medical + Vision.

Now let’s trace what happens when the formula processes each row:

STEP 3 Person & Assignment Lookup

WSA CheckWSA_EXISTS(‘PER_123-45-6789_2024-01-15’)MISS
ActionCall DB → Person# = 100045, Asg# = E12345
WSA SaveWSA_SET(‘PER_…’, 100045)   WSA_SET(‘ASG_…’, E12345)

STEP 4 MultipleEntryCount

WSA CheckWSA_EXISTS(‘MEC_100045_Dental EE Deduction_2024’)MISS
ActionCall DB → MAX = NULL (no existing entry)
ResultMultipleEntryCount = 1   → WSA_SET(‘MEC_…Dental…’, 1)

DB calls: 11 — all cache misses (first time seeing this SSN)

STEP 3 Person & Assignment Lookup

WSA CheckWSA_EXISTS(‘PER_123-45-6789_2024-01-15’)HIT!
ActionWSA_GET → Person# 100045, Asg# E12345 — zero DB calls

STEP 4 MultipleEntryCount — different element name = new WSA key

WSA CheckWSA_EXISTS(‘MEC_100045_Medical EE Deduction_2024’)MISS
ActionCall DB → MAX = NULL
ResultMultipleEntryCount = 1   → WSA_SET(‘MEC_…Medical…’, 1)

DB calls: 4 — Step 3 saved 2 calls (cache hit), Step 4 missed (different element)

STEP 3 Person & Assignment Lookup

WSA CheckWSA_EXISTS(‘PER_123-45-6789_2024-01-15’)HIT!
ActionWSA_GET → Person# 100045, Asg# E12345 — zero DB calls

STEP 4 MultipleEntryCount — yet another element = yet another WSA key

WSA CheckWSA_EXISTS(‘MEC_100045_Vision EE Deduction_2024’)MISS
ActionCall DB → MAX = NULL
ResultMultipleEntryCount = 1

DB calls: 4 — same pattern as Row 2

The Pattern:

Step 3 (Person lookup)Step 4 (MEC)
Row 1MISS — call DBMISS — call DB
Row 2HIT — zero DB callsMISS — different element
Row 3HIT — zero DB callsMISS — different element

Step 3 always hits after Row 1 (same SSN = same key). Step 4 always misses here because each row maps to a different element. Step 4 WSA becomes critical when the batch has multiple rows for the same person + same element.

Performance at Scale

For 10,000 vendor rows where employees average 3 benefit plans each:

value set calls (10K × 11 per row)

63% reduction — Step 3 caching saves ~7 calls per duplicate SSN

Critical Rule: Set Threads = 1

There’s one deployment rule for WSA that you absolutely cannot skip:

If “Load Data from File” runs with 4 threads, each thread gets its own independent WSA:

Step 3 breaks:Thread 1 caches Person# for SSN 123. Thread 2 gets a different row for the same SSN — but Thread 2’s WSA is empty. It calls the value set again. (Wastes performance, but data is still correct.)
Step 4 breaks:Thread 1 assigns MultipleEntryCount = 2 and saves to its WSA. Thread 2 gets another row for the same person+element — but Thread 2’s WSA is empty. It queries the DB, gets MAX = 1, assigns count = 2. Duplicate. Data lost.

The fix:

My Client Groups → Payroll → Payroll Process Configuration → Threads = 1

Set thread count to 1 before running “Load Data from File.” All rows process sequentially in one thread. WSA works as a true shared cache across every row.


Step 5: SourceSystemId Resolution

All resolved values from Steps 1–4→ l_SourceSystemId = ‘HDL_XXVA_E12345_EE_100045_Dental EE Deduction_20240115’

Oracle HDL uses SourceSystemId as the MERGE key. If an entry already exists in cloud, the formula reuses its SourceSystemId (so HDL updates it). If not, it constructs one:

/* For active employees — construct using PersonNumber */
'HDL_XXVA' || l_AssignmentNumber || '_EE_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2

/* For terminated employees (PersonNumber unavailable) — use SSN */
'HDL_XXVA' || l_AssignmentNumber || '_EE_' || POSITION1 || '_' || l_ElementName || '_' || POSITION2

After all five steps, the formula has everything it needs: Element Name, Assignment Number, Person Number, MultipleEntryCount, SourceSystemId, and the dollar amount. Now it generates the HDL output rows (Sections 7 and 8).


Vendor Input (what the formula receives):

POSITION1SSN123-45-6789
POSITION2Date2024-01-15
POSITION3Plan CodeDENTAL01
POSITION4Ded TypePRE
POSITION5Amount150.00
POSITION11Status(blank = Active)

↓ Formula transforms (Steps 1–5 + LINEREPEATNO=1) ↓

HDL .dat Output (ElementEntry):

BusinessOperationMERGE
FileDiscriminatorElementEntry
LegislativeDataGroupName570
EffectiveStartDate2024/01/15
ElementNameDental EE Deduction
AssignmentNumberE12345
CreatorTypeH
EntryTypeE
MultipleEntryCount1
SourceSystemOwnerHDL_XXVA
SourceSystemIdHDL_XXVA_E12345_EE_…

After the five MAP steps, the formula has all the values it needs. Now it generates the actual HDL output. Each vendor source row produces multiple HDL output rows — one ElementEntry header on pass 1, followed by one ElementEntryValue per input value on passes 2 through 7. LINEREPEATNO controls which one gets generated on each pass.

How LINEREPEAT Works

The HDL engine calls the formula once per source row with LINEREPEATNO = 1. If the formula returns LINEREPEAT = 'Y', the engine calls the formula again for the same row — this time with LINEREPEATNO = 2.

/* HDL engine processes one vendor source row: */

/* Pass 1: LINEREPEATNO = 1 → ElementEntry header */
Formula outputs →  MERGE|ElementEntry|...|Dental EE Deduction|...
Formula returns →  LINEREPEAT = 'Y'   ← call me again

/* Pass 2: LINEREPEATNO = 2 → EEV: Amount = 150.00 */
Formula outputs →  MERGE|ElementEntryValue|...|Amount|...|150.00
Formula returns →  LINEREPEAT = 'Y'   ← call me again (more input values)

/* Pass 3: LINEREPEATNO = 3 → EEV: Period Type = Monthly */
Formula outputs →  MERGE|ElementEntryValue|...|Period Type|...|Monthly
Formula returns →  LINEREPEAT = 'Y'   ← call me again

/* ... passes 4–6 for Loan Number, Total Owed, Percentage (if applicable) ... */

/* Pass 7: LINEREPEATNO = 7 → EEV: Deduction Amount (last pass) */
Formula outputs →  MERGE|ElementEntryValue|...|Deduction Amount|...
Formula returns →  LINEREPEAT = 'N'   ← done, move to next source row

One source row → multiple output rows (1 ElementEntry + up to 6 ElementEntryValues). The HDL engine groups all ElementEntry rows together and all ElementEntryValue rows together in the final .dat file, separated by their METADATA header rows.

The .dat Output Structure

The final .dat file has two blocks. Each block starts with a METADATA row that defines the columns, followed by the MERGE data rows:

Block 1 — ElementEntryValue (generated by LINEREPEATNO = 2–7)

A         B                C    D           E                  F               G               J                  K
METADATA  ElementEntryVal  LDG  EffStart    ElementName        AssignmentNum   InputValueName  MultipleEntryCount ScreenEntryValue
MERGE     ElementEntryVal  570  22-09-2019  Dental EE Deduct   123141402543    Amount          3                  150.00
MERGE     ElementEntryVal  222  22-09-2019  Dental EE Deduct   123141402554    Amount          6                  25.72
MERGE     ElementEntryVal  570  22-09-2019  Dental EE Deduct   123141402543    Amount          1                  150.00
...       more rows

Block 2 — ElementEntry (generated by LINEREPEATNO = 1)

A         B             C    D           E                  F               G           I          J
METADATA  ElementEntry  LDG  EffStart    ElementName        AssignmentNum   CreatorType EntryType  MultipleEntryCount
MERGE     ElementEntry  570  22-09-2019  Dental EE Deduct   123141402543    H           E          3
MERGE     ElementEntry  222  22-09-2019  Dental EE Deduct   123141402554    H           E          6
...       more rows

The key columns to notice: ElementEntry has CreatorType and EntryType but no dollar amount. ElementEntryValue has InputValueName (always “Amount”) and ScreenEntryValue (the actual dollar amount like 150.00). Both carry MultipleEntryCount from Step 4.

What LINEREPEATNO = 1 Generates

On the first pass, the formula checks POSITION11 (the STATUS column from the vendor file). This decides whether we’re creating a new entry or end-dating an existing one:

POSITION11ElementEntry row generatedLINEREPEAT
Blank (Active)MERGE|ElementEntry|570|22-09-2019|Dental EE Deduction|123141402543|H||E|1**
EffectiveStartDate = POSITION2. No EndDate. CreatorType = H. EntryType = E.‘Y’→ needs pass 2
C (Cancel)MERGE|ElementEntry|570|22-09-2019|Dental EE Deduction|123141402543|H|2019/09/22|E|1|...|Y
Fetches original StartDate from cloud. Sets EndDate = cancellation date. Appends ReplaceLastEffectiveEndDate = Y.‘N’→ no detail needed

How the Code Actually Writes the ElementEntry Row

The formula does not** use positional output variables like HDL_LINE1_N. Instead, it assigns values to named output variables that match the METADATA column names. Then an explicit RETURN statement tells the HDL engine which variables to pick up and in what order.

Here’s the Active path (POSITION11 is blank):

IF LINEREPEATNO = 1 THEN
(
    /* ======================================== */
    /* ACTIVE entry — create new ElementEntry   */
    /* ======================================== */

    FileName                    = 'ElementEntry'
    BusinessOperation           = 'MERGE'
    FileDiscriminator           = 'ElementEntry'
    LegislativeDataGroupName    = l_LegislativeDataGroupName
    AssignmentNumber            = l_AssignmentNumber
    ElementName                 = l_ElementName
    EffectiveStartDate          = TO_CHAR(TO_DATE(TRIM(POSITION2),'YYYY/MM/DD'),'YYYY/MM/DD')
    EntryType                   = l_entry_type
    CreatorType                 = l_CreatorType
    SourceSystemOwner           = l_SourceSystemOwner
    SourceSystemId              = l_SourceSystemId
    LINEREPEAT                  = 'Y'             /* ← call me again for ElementEntryValue */

    RETURN BusinessOperation, FileName, FileDiscriminator,
           CreatorType, EffectiveStartDate, ElementName,
           LegislativeDataGroupName, EntryType, AssignmentNumber,
           SourceSystemOwner, SourceSystemId,
           LINEREPEAT, LINEREPEATNO
)

How the RETURN works: The variable names in the RETURN statement must match the METADATA column names exactly. The HDL engine maps each returned variable to its corresponding METADATA position and writes the pipe-delimited row in that order. FileName and FileDiscriminator go to positions [1] and [2]. The rest map by name to the METADATA array you defined in Section 5.

For a Cancel row (POSITION11 = ‘C’), the formula fetches the original start date from the cloud, sets an end date, and returns LINEREPEAT = 'N' (no pass 2 needed — you don’t need an ElementEntryValue for a cancellation):

IF (TRIM(POSITION11) = 'C') THEN
(
    /* Fetch the original start date from cloud */
    l_Effective_Start_Date = GET_VALUE_SET('XXVA_GET_EE_START_DATE', ...)

    /* Same named variables, but with end date + replace flag */
    FileName                    = 'ElementEntry'
    BusinessOperation           = 'MERGE'
    FileDiscriminator           = 'ElementEntry'
    EffectiveStartDate          = TO_CHAR(TO_DATE(l_Effective_Start_Date,...),'YYYY/MM/DD')
    EffectiveEndDate            = TO_CHAR(TO_DATE(TRIM(POSITION2),...),'YYYY/MM/DD')
    ReplaceLastEffectiveEndDate = 'Y'
    LINEREPEAT                  = 'N'              /* ← no pass 2 for cancel */
    /* ...same other variables as Active... */

    RETURN BusinessOperation, FileName, FileDiscriminator,
           CreatorType, EffectiveStartDate, EffectiveEndDate,
           ElementName, LegislativeDataGroupName, EntryType,
           AssignmentNumber, SourceSystemOwner, SourceSystemId,
           ReplaceLastEffectiveEndDate,
           LINEREPEAT, LINEREPEATNO
)

Notice the Cancel RETURN includes EffectiveEndDate and ReplaceLastEffectiveEndDate — both absent from the Active RETURN.

Duplicate Header Prevention (WSA)

One person can have multiple vendor rows (Dental, Medical, Vision) that all map to different elements. Each element needs exactly one ElementEntry row. But if two vendor rows map to the same element, the formula must not generate a duplicate header. It checks WSA:

IF WSA_EXISTS('HDR_' || L_PersonNumber || '_' || l_ElementName || '_' || POSITION2) THEN
(
    /* Header already generated for this combo — skip to pass 2 */
    LINEREPEAT = 'Y'
    RETURN
)
/* First time for this combo — generate header, then mark in WSA */
WSA_SET('HDR_' || ..., 1)

Watch out: ISNULL is inverted

The formula checks ISNULL(l_ElementName) = 'N' before generating anything. In Fast Formula, 'N' means the value IS null (not found). If the vendor code didn’t map to any element, the formula skips the row silently.


Same Vendor Input Row → multiple ElementEntryValue outputs (one per input value):

↓ Each pass loads a different InputValueName ↓

Pass 2 — ElementEntryValue (Amount):

InputValueNameAmount
ScreenEntryValue150.00
ElementEntryId(SSID)HDL_XXVA_E12345_EE_… (links to parent ElementEntry)

Pass 3 — ElementEntryValue (Period Type):

InputValueNamePeriod Type
ScreenEntryValueMonthly

Pass 6 — ElementEntryValue (Percentage):

InputValueNamePercentage
ScreenEntryValue5.5

Passes 4, 5, 7 skipped — PRE type doesn’t use Loan Number, Total Owed, or Deduction Amount. The formula returns LINEREPEAT = 'Y' with no output data on those passes.

Passes 2 through 7 each generate one ElementEntryValue row. Each pass loads a different input value. The deduction type (POSITION4) controls which passes produce output and which ones skip.

What LINEREPEATNO = 2 Generates

Each ElementEntryValue pass sets InputValueName to a different value and loads the corresponding data into ScreenEntryValue:

ColumnValueSource
LINEREPEATNOInputValueNameScreenEntryValue source
2Amountl_Amount (POSITION5) = 150.00
3Period Typel_PeriodType (POSITION6) = Monthly
4Loan NumberPOSITION8 — LOAN type only
5Total Owedl_TotalOwed — LOAN type only
6Percentagel_Percentage (POSITION7) — PRE/POST type only
7Deduction Amountl_DeductionAmount — CU type only

How the Code Actually Writes the ElementEntryValue Row

Each pass from 2 to 7 follows the same structure. The key difference is the skip logic: each pass checks POSITION4 (deduction type) to decide whether to generate output or just return LINEREPEAT = 'Y' with no data (effectively skipping to the next pass). Same pattern — named output variables + explicit RETURN. But now FileDiscriminator = 'ElementEntryValue' (not ‘ElementEntry’), and the RETURN includes InputValueName, ScreenEntryValue, and the parent link "ElementEntryId(SourceSystemId)".

ELSE IF (LINEREPEATNO = 2) THEN
(
    l_InputValueName = 'Amount'

    /* Look up ElementEntryValue SourceSystemId from cloud (or construct new one) */
    l_EEV_SourceSystemId = GET_VALUE_SET(
        'XXVA_GET_EEV_SOURCE_SYSTEM_ID', ...)
    l_EEV_SourceSystemOwner = GET_VALUE_SET(
        'XXVA_GET_EEV_SOURCE_SYSTEM_OWNER', ...)

    /* If no existing SSID found, construct a new one */
    IF ISNULL(l_EEV_SourceSystemId) = 'N' THEN
    (
        l_EEV_SourceSystemId = 'HDL_XXVA' || l_AssignmentNumber
            || '_EEV_' || L_PersonNumber
            || '_' || l_ElementName
            || '_' || l_InputValueName
            || '_' || TO_CHAR(TO_DATE(TRIM(POSITION2),...),'YYYYMMDD')
    )

    /* ============================================= */
    /* Set the output variables for ElementEntryValue */
    /* ============================================= */

    FileName                          = 'ElementEntry'        /* always ElementEntry */
    BusinessOperation                 = 'MERGE'
    FileDiscriminator                 = 'ElementEntryValue'   /* ← THIS is the key difference */
    LegislativeDataGroupName          = l_LegislativeDataGroupName
    AssignmentNumber                  = l_AssignmentNumber
    ElementName                       = l_ElementName
    EntryType                         = l_entry_type
    EffectiveStartDate                = TO_CHAR(...)
    "ElementEntryId(SourceSystemId)"  = l_SourceSystemId      /* ← links to parent ElementEntry */
    SourceSystemId                    = l_EEV_SourceSystemId  /* ← EEV's own SSID */
    SourceSystemOwner                 = l_EEV_SourceSystemOwner
    InputValueName                    = l_InputValueName      /* 'Amount' */
    ScreenEntryValue                  = To_Char(TO_NUM(TRIM(l_Amount)))
    LINEREPEAT                        = 'Y'                  /* more passes to come (pass 7 returns 'N') */

    RETURN BusinessOperation, FileName, FileDiscriminator,
           AssignmentNumber, EffectiveStartDate, ElementName,
           EntryType, LegislativeDataGroupName,
           "ElementEntryId(SourceSystemId)",
           InputValueName, ScreenEntryValue,
           SourceSystemOwner, SourceSystemId,
           LINEREPEAT, LINEREPEATNO
)

Three things to notice:

1. FileName is still 'ElementEntry' — NOT 'ElementEntryValue'. Only the FileDiscriminator changes to 'ElementEntryValue'. This is how HDL knows the row goes into the ElementEntryValue block of the .dat file.**

2.** "ElementEntryId(SourceSystemId)" is set to the ElementEntry’s SourceSystemId (l_SourceSystemId). This is the parent-child link. The variable name contains parentheses, so it must be double-quoted in the formula code.**

3.** The ElementEntryValue has its own SourceSystemId (l_EEV_SourceSystemId), different from the parent ElementEntry’s. The formula first tries to find an existing one from the cloud via value set. If not found (ISNULL = 'N'), it constructs one with the pattern: HDL_XXVA + AssignmentNumber + _EEV_ + PersonNumber + _ElementName + _InputValueName + _Date.

The Parent-Child Link

The ElementEntryValue row must reference its parent ElementEntry row. HDL uses SourceSystemId to link them:

ElementEntry (Pass 1)

SourceSystemId = HDL_XXVA_E12345_EE_…

ElementEntryValue (Pass 2)

ElementEntryId(SSID) = HDL_XXVA_E12345_EE_…

SourceSystemId = HDL_XXVA_E12345_EEV_…

The ElementEntryValue’s ElementEntryId(SourceSystemId) matches the ElementEntry’s SourceSystemId. This is how HDL knows which entry this value belongs to.

The RTRIM Trick for Clean Numbers

the vendor sends amounts like 150.00, but Oracle elements expect clean numbers. The formula strips trailing zeros:

RTRIM(RTRIM(TRIM(l_Amount), '0'), '.')

/* 150.00 → 150 | 75.50 → 75.5 | 12.30 → 12.3 | 150.00 → 150.00 */

The inner RTRIM strips trailing zeros. The outer RTRIM strips the decimal point if nothing is left after it.


Vendor CSV Row: 123-45-6789,2024-01-15,DENTAL01,150.00,,,

STEP 1: Type → ER, Amount → 150.00

STEP 2: Key → DENTAL01 → Dental EE Deduction

STEP 3: SSN → Person# 100045, Asg# E12345 (WSA)

STEP 4: MultipleEntryCount = 1

STEP 5: SourceSystemId constructed

LINEREPEATNO=1 → ElementEntry:

MERGE|ElementEntry|570|2019/09/22|Dental EE Deduction|123141402543|H||E|1

LINEREPEATNO=2 → ElementEntryValue (Amount):

MERGE|ElementEntryValue|570|2019/09/22|Dental EE Deduction|123141402543|Amount||E||1|150.00

If you’ve read this far, you can now explain — without looking at any code — how an HDL Transformation Formula works end-to-end. You know what each OPERATION does, why METADATA arrays define the .dat column headers, how the MAP block transforms source data in 5 steps, why WSA exists (performance + correctness), how LINEREPEATNO generates multiple output rows (1 ElementEntry + up to 6 ElementEntryValues) from one source row, and how named RETURN variables map to METADATA columns.

That’s the foundation. The concepts don’t change whether you’re building an vendor deduction interface, a benefits enrollment loader, or a payroll costing feed. Every HDL Transformation Formula follows this same structure.

Coming Next — Part 2: Code Walkthrough

Part 2 takes every concept from this post and shows you the actual Fast Formula code that implements it. Line by line, with the Notepad++ syntax highlighting you’ve been seeing in the code snippets here.

What Part 2 will cover:

Full INPUTS ARE blockEvery POSITION mapped to its vendor column, every DEFAULT FOR explained
GET_VALUE_SET callsThe exact parameter string construction with pipe delimiters, date conversions, and ISNULL checking
WSA implementationReal WSA_EXISTS / WSA_GET / WSA_SET code with key construction patterns
SourceSystemId logicThe full lookup-or-construct pattern for both ElementEntry and ElementEntryValue SourceSystemIds
ESS_LOG_WRITE debuggingAdding trace logs at each step so you can debug formula execution in real time
Cancel vs Active branchingThe complete IF POSITION11 = ‘C’ block with date fetching from cloud

Later — Part 3: Build Your Own

Part 3 is the implementation guide. You’ll build an HDL Transformation Formula from scratch — from creating the formula in Oracle Cloud, defining all 11 value sets, configuring the HDL integration, running test loads, reading ESS logs, and troubleshooting the errors you’ll hit in production.

After Part 3, you’ll have a working formula you can adapt for any inbound payroll interface — not just one vendor.

Series Roadmap

Part 1: Pure Concepts ← This post

Part 2: Code Walkthrough Coming soon

Part 3: Build Your Own Coming soon

Abhishek Mohanty