Kysoql
Build and run queries

Mental model

Understand immutability, query modes, metadata, and result inference.

A few ideas make the rest of Kysoql much easier to reason about.

Kysely inspires the API, not the query language

Kysoql borrows fluent, immutable TypeScript patterns from Kysely where they fit. It does not try to turn SOQL into SQL.

Familiar ideaKysoql behaviour
selectFrom, select, where, orderBy, limit, offsetSimilar fluent names where SOQL has a matching concept.
SQL joinsUse Salesforce parent paths, child subqueries, and supported semi-joins or anti-joins.
SQL placeholdersOrdinary API values are escaped directly into compiled SOQL.
Mutations and transactionsOutside the scope of a SOQL query builder.
Kysely pluginsKysoql has its own KysoqlPlugin contract.
Raw SQLsoql.raw() is Kysoql's explicit unsafe SOQL escape hatch.

Apex binds are different from ordinary API values. They are available only in the compile-only Apex builders.

Your schema describes the org to TypeScript

new Kysoql<YourSchema>() uses the schema type to constrain what you can build. The usual schema comes from @kysoql/codegen and reflects Salesforce metadata such as fields, relationships, nullability, and query capabilities.

The TypeScript type itself is compile-time metadata. For features that need runtime metadata, such as Big Object index validation, pass the generated salesforceSchemaMetadata value as shown in the codegen guide.

Do not use the broad helper types from @kysoql/core as a replacement for an org-specific generated schema. They cannot know your custom objects or fields.

Builders are immutable

Every query method returns a new builder:

import { db } from "./db";

const base = db.selectFrom("Account").select(["Id", "Name"]);
const acme = base.where("Name", "like", "Acme%");
const firstPage = acme.orderBy("Name").limit(25);

console.log(firstPage.compile().soql);

base still has no WHERE, ORDER BY, or LIMIT. That makes it safe to derive several queries from one starting point.

Query modes narrow the API

Kysoql changes the available methods as the query shape becomes more specific.

ModeHow you enter itResult or purpose
Record querySelect normal fields or supported SELECT functionsReadonly array of selected records.
Aggregate querySelect an aggregate or group firstReadonly array of aggregate rows.
Scalar countSelect bare fn.count()One number.
Relationship subqueryUse selectSubquery()Nested relationship result in the parent row.
Semi-joinUse an in or not in subquery callbackA filter only.
ApexCall apex() or dynamicApex() at the supported pointCompile-only SOQL.

Methods disappear when they would no longer describe valid SOQL. A bare count, for example, does not expose the same pagination API as a normal record query.

Result types follow selections

import { db } from "./db";

const query = db
  .selectFrom("Account")
  .select(["Id", "Name as name", "Owner.Name"]);

The result type contains only what you selected.

Generated nullability carries through to field and relationship types. A field added inside $if becomes optional because it might not be selected at runtime. Aliases create the aliased property name in the result.

The bundled executors validate the Salesforce response envelope, not every selected field value at runtime. Types are not an authorization or runtime validation layer. See security and production use.

Compile or execute

compile().soql gives you the final SOQL string.

execute() needs a configured executor. The bundled executors follow root pagination for you. .executeAll() switches to Salesforce QueryAll mode rather than meaning "fetch all pages".

Use native REST execution for the normal transport and compile for Apex for Apex-only SOQL.

About the examples

Most guide snippets import db from the getting started guide.

Generate every object and field an example uses before running it. Names such as Office__c and Interests__c are illustrative custom metadata and should be replaced with API names from your org.

On this page