Jeff Benedict
Tera Contributor

One of our recent ServiceNow design challenges looked simple on the surface: capture two values in different currencies, convert both to U.S. dollars, and determine whether there was a difference in the values. The complication was that the comparison had to use the exchange rate that applied on the transaction date, not the rate in effect when someone later edited the record.

 

The Challenge

We initially looked at ServiceNow FX Currency fields. They are useful when a record needs a currency amount and an associated reference value, but they did not fit this accounting scenario. If we corrected a value or balance changed later, the converted value would be recalculated using the current date. That meant the same transaction could produce a different USD value simply because the record was updated on a different day.


In our scenario, the conversion date is part of the business record. The rate needs to be reproducible and explainable later. We therefore needed to stamp the transaction date, find the exchange rate whose effective period covered that date, and retain enough information on the record to understand how the USD value was calculated.


The Data Model

Rather than rely on a single FX Currency field, we separated the source amount from the conversion result. Each partner balance uses its own fields for currency, source value, USD value, applied rate, and selected rate record. We also store the conversion date, calculation timestamp, status, and message.

Field purpose

Example

Why it matters

Source currency

EUR or BRL

Identifies the currency of the original balance.

Source balance

2,000 or -5,000

Preserves the value as supplied by the source process.

Balance in USD

2,243 or -996.70

Provides the common basis for comparison.

Applied FX rate

1.1215 or 0.199341

Makes the conversion visible on the record.

FX rate record

Reference to the selected rate

Provides traceability to the rate used.

Conversion date

2026-10-21

Locks the calculation to the business date.

Status and message

Calculated / success message

Makes failures and incomplete rates operationally visible.

What the Record Looks Like

The example below shows two partner balances on the same relationship record. Partner A is recorded in EUR and Partner B in BRL. ServiceNow stores the original balances, converts both amounts to USD using rates that cover October 21, 2026, and records the rate details and calculation outcome.

JeffBenedict_0-1792685737147.png

 

Figure 1. Example intercompany relationship balance after historical FX conversion.


The Conversion Logic

The conversion logic is centralized in a reusable Script Include. The caller supplies the amount, three-character source currency, and conversion date. The utility validates those inputs, handles USD without a lookup, searches for a direct source-to-USD rate, and then falls back to a reverse USD-to-source rate when needed.

 

var fxUtil = new IntercompanyFXUtil();

var resultA = fxUtil.convertToUSD(
    current.getValue('ic_partner_a_balance'),
    getCurrencyCode(current, 'ic_partner_a_currency'),
    current.getValue('date'),
    {}
);

var resultB = fxUtil.convertToUSD(
    current.getValue('ic_partner_b_balance'),
    getCurrencyCode(current, 'ic_partner_b_currency'),
    current.getValue('date'),
    {}
);

 

The important part is the third parameter. The conversion date comes from the relationship-balance record. The calculation is therefore tied to the transaction date even if the record is entered or updated later.

 

Selecting the Rate That Covers the Date

The rate lookup queries the ServiceNow exchange-rate table for the currency pair and checks that the selected date falls inside the rate's effective span. If more than one eligible rate overlaps the date, the query uses rate order, effective start, and creation time to produce a result.

 

rateGR.addQuery('from_currency', fromCurrency);
rateGR.addQuery('to_currency', toCurrency);
rateGR.addQuery('span_start', '<=', effectiveDate + ' 23:59:59');

var endCondition = rateGR.addQuery(
    'span_end',
    '>=',
    effectiveDate + ' 00:00:00'
);
endCondition.addOrCondition('span_end', '');

rateGR.orderByDesc('order');
rateGR.orderByDesc('span_start');
rateGR.orderByDesc('sys_created_on');
rateGR.setLimit(1);

 

 

Handling Direct and Reverse Rates

The utility first tries to find a rate from the source currency to USD. When that rate exists, the source amount is multiplied by the rate. If only the reverse pair is available, the amount is divided by the USD-to-source rate.

 

if (directRate.success) {
    result.rate = directRate.rate;
    result.usdAmount = this._round(
        numericAmount * directRate.rate,
        2
    );
    result.calculationMethod = 'DIRECT_MULTIPLY';
}

if (reverseRate.success) {
    result.rate = reverseRate.rate;
    result.usdAmount = this._round(
        numericAmount / reverseRate.rate,
        2
    );
    result.calculationMethod = 'REVERSE_DIVIDE';
}

 

Writing the Result Back to the Record

A before Business Rule runs when either partner balance, either currency, or the transaction date changes. The rule calculates both USD values and writes the converted balance, applied rate, rate record, conversion date, timestamp, status, and message back to the same record.

 

function applyResult(record, result, usdField, rateField, rateRecordField) {
    if (result.success) {
        record.setValue(usdField, result.usdAmount);
        record.setValue(rateField, result.rate);
        record.setValue(rateRecordField, result.rateRecordSysId || '');
    } else {
        record.setValue(usdField, '');
        record.setValue(rateField, '');
        record.setValue(rateRecordField, '');
    }
}

current.setValue('fx_conversion_date', conversionDate);
current.setValue('fx_calculated_on', new GlideDateTime());

 

Only after both conversions succeed should the process calculate the variance. In our sign convention, one partner's receivable and the other partner's payable are stored with opposite signs, so the comparison is the sum of the two USD values. If both sides are stored as positive balances, the formula should instead subtract one side from the other.

 

if (resultA.success && resultB.success) {
    var amountA = parseFloat(resultA.usdAmount);
    var amountB = parseFloat(resultB.usdAmount);
    var varianceUsd = roundCurrency(amountA + amountB);

    current.setValue('fx_calculation_status', 'calculated');
    current.setValue(
        'fx_calculation_message',
        'Partner A and Partner B balances converted successfully.'
    );
}

 

Why This Pattern Worked Better

  • The conversion is repeatable because the transaction date is explicitly stored and reused.
  • The original currency and source amount remain visible instead of being hidden behind a reference currency calculation.
  • The applied rate and rate record provide a practical audit trail for reconciliation and support.
  • A reusable Script Include keeps rate-selection logic out of individual Business Rules and flows.
  • Calculation status and messages make missing or invalid rate data visible before a variance is treated as final.

Implementation Considerations

A few details are worth deciding before using this pattern broadly:

  • Confirm whether a blank rate end date means the rate remains open-ended.
  • Define a clear precedence rule if multiple exchange-rate records cover the same day.
  • Decide whether ERP source and rate type should be mandatory filters for the rate lookup.
  • Use an appropriate decimal strategy when accounting precision requires more than display-level rounding.


Final Thoughts

Currency comparison is not only a math problem. It is also a date-effective data problem. Once the requirement became 'compare both balances using the rate that applied on the transaction date,' relying on the platform's current-date currency behavior was no longer enough.

Separating the source currency, source amount, converted amount, rate, and conversion date gave us a more transparent and supportable design. It also made the result easier to explain: both partner balances are converted to the same currency using rates that were valid on the same business date, and the variance can then be calculated consistently.

Hope this is helpful for others working with currency conversion in ServiceNow.