Skip to main content

Record Context

Every formula is evaluated against a FormulaContext — the object that represents the Salesforce record and its environment.

Basic Fields

The record property is a flat object mapping field names to their values — the same shape as a SOQL query result:

import { evaluateFormula } from '@jetstreamapp/sf-formula-parser';

evaluateFormula('Name & " - $" & TEXT(Amount)', {
record: {
Name: 'Acme Corp',
Amount: 50000,
},
});
// "Acme Corp - $50000"

Field values can be any FormulaValue:

TypeExample
number42, 3.14
string"Acme Corp"
booleantrue, false
Datenew Date('2024-01-15')
SfTime{ timeInMillis: 43200000 } (noon)
GeoLocation{ latitude: 37.7749, longitude: -122.4194 }
nullnull (blank field)

Access fields on related records using dot-notation in the formula. Related records are nested directly in the record object:

// Formula: Account.Name
evaluateFormula('Account.Name', {
record: {
Account: { Name: 'Acme Corp' },
},
});
// "Acme Corp"

Multi-Level Relationships

Relationships can be nested:

// Formula: Contact.Account.Industry
evaluateFormula('Contact.Account.Industry', {
record: {
Contact: {
Account: { Industry: 'Technology' },
},
},
});
// "Technology"

Global Variables

Salesforce global variables like $User, $Profile, and $Organization are supported via the globals property:

// Formula: $User.FirstName
evaluateFormula('$User.FirstName', {
record: {},
globals: {
$User: {
FirstName: 'Jane',
LastName: 'Smith',
Email: 'jane@example.com',
},
},
});
// "Jane"

Prior Value Support

For trigger-context formulas that use ISCHANGED, PRIORVALUE, ISNEW, or ISCLONE:

// ISCHANGED(Status) - true when Status differs from prior value
evaluateFormula('ISCHANGED(Status)', {
record: { Status: 'Closed' },
priorRecord: { Status: 'Open' },
});
// true

// PRIORVALUE(Amount) - returns the previous value
evaluateFormula('PRIORVALUE(Amount)', {
record: { Amount: 200 },
priorRecord: { Amount: 100 },
});
// 100

// ISNEW() - true when the record is newly created
evaluateFormula('ISNEW()', {
record: {},
isNew: true,
});
// true

// ISCLONE() - true when the record is a clone
evaluateFormula('ISCLONE()', {
record: {},
isClone: true,
});
// true

Evaluation Options

Pass options as the third argument to evaluateFormula:

evaluateFormula(formula, context, {
returnType: 'string',
schema: describe.fields,
treatBlanksAsZeroes: true, // default
now: new Date('2024-06-15T12:00:00Z'),
});

treatBlanksAsZeroes

Default: true

When enabled (the Salesforce default), null/blank values are coerced:

  • Blank numbers become 0
  • Blank text becomes ""

When disabled, null values propagate through calculations, which can be useful for detecting missing data.

now

Override the timestamp returned by NOW() and TODAY(). Essential for writing deterministic tests:

evaluateFormula('TEXT(TODAY())', context, {
now: new Date('2024-06-15T12:00:00Z'),
});
// Returns a consistent result regardless of when the test runs

Schema Validation

Pass Salesforce field metadata to enable type-aware validation. The schema option accepts an array of FieldSchema objects — compatible with describeSObject().fields:

const describe = await conn.describeSObject('Account');

evaluateFormula('UPPER(Name)', context, {
schema: describe.fields, // pass directly — no transformation needed
});

When schema is provided:

  • Field existence — referencing a direct field not in the schema throws FormulaError
  • Picklist restrictions — picklist fields can only be used in TEXT(), ISPICKVAL(), CASE(), ISBLANK(), ISNULL(), NULLVALUE(), BLANKVALUE(), INCLUDES(), ISCHANGED(), PRIORVALUE()
  • All other behavior is unchanged — schema is purely additive

Simple schema (current object only)

A flat FieldSchema[] validates only direct fields on the current object:

const schema = [
{ name: 'Name', type: 'string' },
{ name: 'Status', type: 'picklist' },
];

evaluateFormula('Name', context, { schema }); // OK
evaluateFormula('MissingField', context, { schema }); // throws
evaluateFormula('Account.Name', context, { schema }); // OK (no schema for Account, bypassed)

Pass a Record<string, FieldSchema[]> to validate related object fields and globals too. Use '$record' for the root object, relationship names as keys, and $-prefixed names for globals:

const schema = {
$record: describeContact.fields, // current object
Account: describeAccount.fields, // Account relationship
$User: describeUser.fields, // $User global
};

evaluateFormula('Name', context, { schema }); // validated against root schema
evaluateFormula('Account.Name', context, { schema }); // validated against Account schema
evaluateFormula('$User.FirstName', context, { schema }); // validated against $User schema
evaluateFormula('Owner.Name', context, { schema }); // bypassed (Owner not in schema)
evaluateFormula('Account.Website', context, { schema }); // throws (not in Account schema)

Relationships not included in the schema map bypass validation — you only need to provide schemas for the objects you want validated.

Full Interface

// A flat record — fields and related records coexist as keys
type FormulaRecord = { [key: string]: FormulaValue | FormulaRecord };

interface FormulaContext {
record: FormulaRecord;
globals?: Record<string, FormulaRecord>;
priorRecord?: FormulaRecord;
isNew?: boolean;
isClone?: boolean;
}

// Schema can be a flat array (current object) or a map (multiple objects)
type SchemaInput = FieldSchema[] | Record<string, FieldSchema[]>;

interface EvaluationOptions {
returnType?: FormulaReturnType;
schema?: SchemaInput;
treatBlanksAsZeroes?: boolean;
now?: Date;
}

type FormulaValue = number | string | boolean | Date | SfTime | GeoLocation | null;