Apex bindings and expressions
Build typed Apex variable references without dropping down to raw SOQL.
The helpers on this page are exported by @kysoql/core and become available
after .apex() or .dynamicApex(). They describe expressions that your Apex
caller will provide at runtime.
Scalar and collection binds
import { apexBind } from "@kysoql/core";
import { db } from "./db";
const compiled = db
.selectFrom("Account")
.select(["Id", "Name"])
.apex()
.where((eb) =>
eb.or([
eb("Name", "=", apexBind<string>("filters.accountName")),
eb("Id", "in", apexBind<readonly string[]>("accountIds")),
]),
)
.compile();apexBind<T>(expression) accepts an Apex identifier or dotted member path.
Leave off the leading colon. Calls, indexing, whitespace, and arbitrary
expression text are rejected.
The generic T checks compatibility with the surrounding query. It cannot prove
that the Apex variable exists or has a valid value at runtime.
Limits and offsets
import { apexBind } from "@kysoql/core";
import { db } from "./db";
const compiled = db
.selectFrom("Account")
.select("Id")
.apex()
.limit(apexBind<number>("page.rowLimit"))
.offset(apexBind<number>("page.rowOffset"))
.compile();Literal limits and offsets are validated by Kysoql. Bound values only exist in Apex, so validate their runtime range there.
Addition and concatenation
Use apexAdd() for supported string concatenation or numeric addition:
import { apexAdd, apexBind } from "@kysoql/core";
import { db } from "./db";
const compiled = db
.selectFrom("Account")
.select("Id")
.apex()
.where("Name", "=", apexAdd("Ac", "me"))
.limit(apexAdd(apexBind<number>("page.baseLimit"), 1))
.compile();This helper is intentionally small. It is not an arbitrary Apex expression parser.
String substring
import { apexAdd, apexBind, apexSubstring } from "@kysoql/core";
import { db } from "./db";
const compiled = db
.selectFrom("Account")
.select("Id")
.apex()
.where(
"Name",
"like",
apexAdd(apexSubstring(apexBind<string>("filters.name"), 0, 2), "%"),
)
.compile();apexSubstring() accepts a string literal or string-valued bind expression and
non-negative literal indices. Runtime string length still belongs to the Apex
caller.
Read a field from a single-record query
import { apexBind, apexQueryField } from "@kysoql/core";
import { db } from "./db";
const sourceAccount = db
.selectFrom("Account")
.select("Name")
.apex()
.where("Id", "=", apexBind<string>("sourceAccount.Id"));
const compiled = db
.selectFrom("Account")
.select("Id")
.apex()
.where("Name", "=", apexQueryField(sourceAccount, "Name"))
.compile();apexQueryField() models Apex bracket-query field access. Kysoql does not add
LIMIT 1, so make sure the inner query is constrained to exactly one record.
Bind-left INCLUDES
Apex also supports Kysoql's bind-left includes overload:
import { apexBind } from "@kysoql/core";
import { db } from "./db";
const compiled = db
.selectFrom("Account")
.select("Id")
.apex()
.where(apexBind<string>("account.Type"), "includes", [
"Customer - Direct; Customer - Channel",
])
.compile();This is a specific overload for includes, not a general bind-left filter API.
Binds in child subqueries
Enter Apex before creating the child subquery when its filters need binds:
import { apexBind } from "@kysoql/core";
import { db } from "./db";
const compiled = db
.selectFrom("Account")
.select("Id")
.apex()
.selectSubquery("Contacts", (contacts) =>
contacts
.select(["Id", "LastName"])
.where("LastName", "like", apexBind<string>("filters.lastName")),
)
.compile();Child builders remain child builders. They do not gain root-only Apex features. Knowledge article Apex queries also reject bind expressions.
See Compile for Apex for context rules and support boundaries for broader restrictions.