Kysoql
Build and run queries

Filtering records

Use typed comparisons, grouped logic, lists, and multipicklist filters.

Basic conditions

Repeated where calls are joined with AND:

import { db } from "./db";

const query = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where("Name", "like", "Acme%")
  .where("AnnualRevenue", ">=", 100_000);

Pass application values through the builder instead of constructing SOQL strings yourself. Kysoql validates and escapes ordinary API values when it compiles the query.

Do not pre-quote strings. Apex compile-only queries have separate typed bind helpers when you need Apex variables rather than SOQL literals.

Operators and values

OperatorAccepted values
=, !=Values compatible with the generated field. null is allowed only where the field supports it.
<, <=, >, >=Ordered field types with non-null values.
likeSupported string-like fields and a string pattern.
in, not inNon-empty homogeneous lists or compatible ID/reference subqueries.
includes, excludesNon-empty generated active values for multipicklist fields.

Operator names are lowercase in TypeScript. Compiled SOQL uses the normal uppercase Salesforce keywords.

For null checks, use where("Field", "=", null) or "!=". Do not pass the string "null".

Date and time fields use typed literal helpers. Location fields use distance expressions.

Group AND, OR, and NOT

The expression builder can be called as eb(field, operator, value) and also provides and, or, and not:

import { db } from "./db";

const query = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where((eb) =>
    eb.and([
      eb.or([
        eb("Name", "like", "Acme%"),
        eb("Name", "like", "Example%"),
      ]),
      eb.not(eb("Name", "like", "%Test%")),
    ]),
  );

Keep boolean groups non-empty. If a filter is optional, $if is usually clearer than trying to define an empty and([]) or or([]).

IN and NOT IN

import { db } from "./db";

const ids: readonly string[] = ["001000000000001AAA"];

if (ids.length === 0) {
  throw new Error("Choose at least one account ID");
}

const query = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where("Id", "in", ids);

Empty lists are rejected. Decide what an empty application input means before building the query. It might mean "no matches", "invalid request", or "skip this optional filter".

Be especially careful with access-control filters. Silently dropping an empty authorization list can widen a query.

For related-record existence checks, use a semi-join or anti-join instead of fetching IDs into JavaScript first.

Multipicklists

This example assumes Account.Interests__c is a generated multipicklist with active values Email and Events:

import { soqlMultiSelectAnd } from "@kysoql/core";
import { db } from "./db";

const eitherValue = db
  .selectFrom("Account")
  .select(["Id", "Interests__c"])
  .where("Interests__c", "includes", ["Email", "Events"]);

const bothValues = db
  .selectFrom("Account")
  .select(["Id", "Interests__c"])
  .where(
    "Interests__c",
    "includes",
    soqlMultiSelectAnd("Email", "Events"),
  );

A normal array uses Salesforce's OR form. soqlMultiSelectAnd(...) produces the AND form while staying typed against generated active picklist values.

Apex also has a separate bind-left INCLUDES form.

Treat LIKE patterns deliberately

Plain strings keep Salesforce LIKE behaviour, so % and _ remain wildcard characters:

import { db } from "./db";

const query = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where("Name", "like", "Acme_%");

When user input should be matched literally, use soql.likeLiteral():

import { soql } from "@kysoql/core";
import { db } from "./db";

const search = "50%_off";

const query = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where("Name", "like", soql.likeLiteral(search));

Kysoql validates identifiers and escapes values, but that is not authorization. If users can choose fields or filters, keep an application allowlist for what they are allowed to query. See security and production use.

Raw SOQL escape hatch

Use soql.raw() only for trusted syntax the builder does not model:

import { soql } from "@kysoql/core";
import { db } from "./db";

const query = db
  .selectFrom("Account")
  .select(["Id", "Name"])
  .where(soql.raw("CALENDAR_YEAR(CreatedDate) = 2026"));

Raw fragments bypass normal identifier validation and value escaping. Never interpolate request data, search terms, IDs, form values, or other untrusted input into them.

Filter with functions

Expression callbacks expose supported date functions, toLabel, and distance through eb.fn. Beta arithmetic lives under eb.beta so experimental syntax is easy to spot.

Continue with date functions, translated labels, or Beta and pilot features.

On this page