@jetstreamapp/sf-formula-parser
v2.0.0
Published
Salesforce formula parser and evaluator — parse and execute Salesforce formula expressions in JavaScript
Readme
sf-formula-parser
Built 100% by Claude Code — lexer, parser, evaluator, 90+ functions, tests, and docs.
Some tests cases were extracted from formula-engine to validate actual Salesforce behavior.
A JavaScript/TypeScript implementation of the Salesforce formula language. Parses formula strings into an AST and evaluates them against a record context — entirely client-side, with zero dependencies.
Why?
Salesforce formulas are powerful, but evaluating them outside of Salesforce (in custom UIs, offline tools, testing harnesses, or migration scripts) has historically required either round-tripping to the API or reimplementing logic by hand. This library gives you a spec-faithful formula engine that runs anywhere JavaScript runs — browsers, Node.js, edge functions.
The implementation was built by studying the open-source Salesforce formula engine to match real-world behavior, not just documentation.
Features
- 90+ functions across logical, math, text, and date/time categories
- All operators — arithmetic (
+,-,*,/,^), comparison (=,!=,<,>,<=,>=), logical (&&,||,!), and string concatenation (&) - Case-insensitive —
IF,if,Ifall work, just like Salesforce - Lazy evaluation —
IF,CASE,IFS, and other branching functions only evaluate the branch that's taken - Related record traversal —
Account.Name,Contact.Account.Industry - Prior value support —
ISCHANGED,PRIORVALUE,ISNEW,ISCLONE - Return type validation — optionally declare the expected return type (
number,string,boolean,date,datetime,time) and get Salesforce-accurate type mismatch errors - Schema-aware validation — pass
describeSObject().fieldsdirectly to enable field existence checks and picklist restrictions - Strict operator type checking — arithmetic operators reject booleans and strings, matching Salesforce behavior
- Zero dependencies — pure TypeScript, compiles to ESM
- Browser-compatible — no Node.js APIs required
Installation
npm install @jetstreamapp/sf-formula-parserQuick Start
import { evaluateFormula } from '@jetstreamapp/sf-formula-parser';
const result = evaluateFormula('IF(Amount > 1000, "Large", "Small")', {
record: {
Amount: 5000,
},
});
// result: "Large"API
evaluateFormula(formula, context, options?)
Parse and evaluate a formula in one call.
import { evaluateFormula } from '@jetstreamapp/sf-formula-parser';
const result = evaluateFormula('UPPER(LEFT(Name, 3))', {
record: { Name: 'Acme Corp' },
});
// result: "ACM"parseFormula(formula)
Parse a formula string into an AST without evaluating it. Useful for caching, inspection, or building tooling on top of formulas.
import { parseFormula } from '@jetstreamapp/sf-formula-parser';
const ast = parseFormula('1 + 2 * 3');createEvaluator(registry?, context?, options?)
Create a reusable evaluator instance. Useful when evaluating many formulas against the same context or with a custom function registry.
import { createEvaluator, createDefaultRegistry } from '@jetstreamapp/sf-formula-parser';
const evaluator = createEvaluator(createDefaultRegistry(), {
record: { Status: 'Active', Amount: 100 },
});Record Context
The FormulaContext object represents the record a formula is evaluated against:
interface FormulaContext {
record: FormulaRecord; // field values and related records
globals?: Record<string, FormulaRecord>; // e.g., $User.Id
priorRecord?: FormulaRecord; // for ISCHANGED / PRIORVALUE
isNew?: boolean; // for ISNEW()
isClone?: boolean; // for ISCLONE()
}
// A flat record — fields and related records coexist as keys, just like a SOQL result
type FormulaRecord = { [key: string]: FormulaValue | FormulaRecord };FormulaValue can be number, string, boolean, Date, SfTime, GeoLocation, or null.
Related Records
Related records are nested directly in the record — no separate related property needed:
evaluateFormula('Account.Industry', {
record: {
Account: { Industry: 'Technology' },
},
});
// result: "Technology"Options
interface EvaluationOptions {
returnType?: FormulaReturnType; // validate result type ('number' | 'string' | 'boolean' | 'date' | 'datetime' | 'time')
schema?: SchemaInput; // flat FieldSchema[] or Record<string, FieldSchema[]> for related/global schemas
treatBlanksAsZeroes?: boolean; // default: true (matches Salesforce default)
now?: Date; // override current time for deterministic tests
}Return Type Validation
Declare the expected return type to catch type mismatches, just like Salesforce:
// Passes — formula returns a number
evaluateFormula('Amount + 1', { record: { Amount: 100 } }, { returnType: 'number' });
// Throws — formula returns a Date, but number was expected
evaluateFormula('CreatedDate + 1', { record: { CreatedDate: new Date() } }, { returnType: 'number' });
// FormulaError: Formula result is data type (Date), incompatible with expected data type (Number).Schema Validation
Pass Salesforce field metadata for field existence checks and picklist restrictions:
import { evaluateFormula } from '@jetstreamapp/sf-formula-parser';
// Fields from describeSObject() — pass directly, no transformation needed
const schema = [
{ name: 'Name', type: 'string' },
{ name: 'Amount', type: 'currency' },
{ name: 'Status', type: 'picklist' },
];
// Field existence check
evaluateFormula('MissingField', { record: {} }, { schema });
// FormulaError: Field MissingField does not exist. Check spelling.
// Picklist restriction (matches Salesforce behavior)
evaluateFormula('Status & " test"', { record: { Status: 'Active' } }, { schema });
// FormulaError: Field status is a picklist field. Picklist fields are only supported in certain functions.
// Use ISPICKVAL instead
evaluateFormula('ISPICKVAL(Status, "Active")', { record: { Status: 'Active' } }, { schema });
// trueRelated Object & Global Schema
Pass a Record<string, FieldSchema[]> to validate related object fields and globals too. Use '$record' for the root object, relationship names for related objects, and $-prefixed names for globals:
const schema = {
$record: describeContact.fields, // current object (Contact)
Account: describeAccount.fields, // Account relationship
$User: describeUser.fields, // $User global
};
evaluateFormula('Account.Name', context, { schema }); // validated against Account schema
evaluateFormula('$User.FirstName', context, { schema }); // validated against $User schema
evaluateFormula('Account.MissingField', context, { schema });
// FormulaError: Field MissingField does not exist. Check spelling.Relationships not included in the schema map bypass validation — you only need to provide schemas for the objects you want validated.
Supported Functions
| Category | Functions |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Logical (16) | IF, IFS, CASE, AND, OR, NOT, ISBLANK, ISNULL, ISNUMBER, BLANKVALUE, NULLVALUE, IFERROR, ISCHANGED, ISNEW, ISCLONE, PRIORVALUE |
| Math (19) | ABS, CEILING, FLOOR, MCEILING, MFLOOR, EXP, LN, LOG, MAX, MIN, MOD, PI, POWER, RAND, ROUND, SQRT, TRUNC, DISTANCE, GEOLOCATION |
| Text (27) | BEGINS, BR, CASESAFEID, CONTAINS, FIND, GETSESSIONID, HTMLENCODE, HYPERLINK, IMAGE, INCLUDES, ISPICKVAL, JSENCODE, JSINHTMLENCODE, LEFT, LEN, LOWER, LPAD, MID, RIGHT, RPAD, SUBSTITUTE, TEXT, TRIM, UPPER, URLENCODE, VALUE, REGEX |
| Date/Time (17) | ADDMONTHS, DATE, DATEVALUE, DATETIMEVALUE, DAY, HOUR, MILLISECOND, MINUTE, MONTH, NOW, SECOND, TIMENOW, TIMEVALUE, TODAY, WEEKDAY, YEAR |
Architecture
Formula String → Lexer → Tokens → Parser → AST → Evaluator → FormulaValue
↑
FormulaContext + Options- Lexer — tokenizes the formula string, handling string escapes, comments, and operators
- Parser — hand-rolled Pratt parser (top-down operator precedence) producing a discriminated union AST
- Evaluator — tree-walking evaluator with lazy argument evaluation and a pluggable function registry
Error Handling
The library throws typed errors you can catch and inspect:
import { FormulaError, LexerError, ParseError } from '@jetstreamapp/sf-formula-parser';
try {
evaluateFormula('IF(', { record: {} });
} catch (e) {
if (e instanceof ParseError) {
// syntax error
} else if (e instanceof FormulaError) {
// runtime evaluation error
}
}License
MIT
