Skip to main content

TypeScript SDK

TypeScript SDK reference to build your own generators.

If you are adapting reusable Playwright flows into Sixpack generators, read Convert an existing Playwright repo alongside this reference. For a fresh Playwright-based supplier, start with Start from an empty Playwright project.

For Playwright-backed suppliers, validate important flows in three modes:

  • direct Playwright test execution
  • standalone sixpack-playwright generate
  • live supplier execution

The third mode exposes retry, timeout, and stale-page issues that ordinary Playwright test execution often does not.

When Playwright flows are adapted into generators, add sixpack-playwright-adapter validate ./sixpack/generators.ts to CI so the generator inputSchema stays aligned with the flow consumer contract.

Define a supplier and a generator

The minimum setup is to have one supplier with one generator. Those can be defined as follows:

import { Supplier } from 'sixpack-sdk'
import { defineGeneratorItem, s } from 'sixpack-sdk/item'

// Define the input schema
const inputSchema = {
country: s.select('Czechia', 'France', 'Italy', 'Hungary', 'Poland').optional(),
language: s.string().describe('Language of the invoice'), // field is required by default
amountToBill: s.number().describe('Amount on the invoice').required(),
extendedTerm: s.boolean().describe('Should term be extended').optional(),
vatId: s.string().optional(),
}

type InvoiceRequest = s.infer<typeof inputSchema>

// Define the output schema
const outputSchema = {
invoiceId: s.string(),
}

// Define the generate function
function generate(input: InvoiceRequest): s.infer<typeof outputSchema> {
const randomId = String(Date.now() % 1000)
return { invoiceId: randomId }
}

// Define the generator item
const invoiceGenerator = defineGeneratorItem({
generate,
metadata: {
name: 'Invoice',
inputSchema,
outputSchema,
// reportIssueEmail: is inherited from the parent supplier
},
})

// Bootstrap the supplier
async function main() {
const supplier = new Supplier({
name: 'BillingSystem',
reportIssueEmail: 'developer@sixpack.dev',
}).withGenerators(invoiceGenerator)

await supplier.bootstrap()
}

main().catch(console.error)

It is required that the supplier is an instance of Supplier and that generators implement the GeneratorItem interface.

The SupplierMetadata / ItemMetadata has the following mandatory attributes:

  • name that uniquely identifies the supplier, it is case-sensitive
  • reportIssueEmail or reportIssueUrl that allows users to report issues with the given supplier

The full list of attributes is:

  • Unique attributes
    • name that uniquely identifies the supplier or the item, it is case-sensitive
    • description that describes the supplier or the item
  • Attributes inheritable from parent
    • maintainer that specifies the person or team responsible for the supplier or the item
    • reportIssueEmail that is intended to open an email client with a pre-filled email
    • reportIssueUrl that is intended to open a page where an issue can be reported
    • alertEmails specifies emails to which information about the supplier status is sent to.

Note: only one of reportIssueEmail or reportIssueUrl is mandatory but both can be specified.

Quickstart checklist before first bootstrap():

  • supplier name
  • supplier reportIssueUrl or reportIssueEmail
  • generator metadata.name
  • generator inputSchema
  • generator outputSchema
  • generator outputKind when the output is not the default FLAT
  • Sixpack URL, account, environment, auth token, and cert/key paths

SIXPACK_AUTH_TOKEN is required even when you also provide client certificate and key paths. The token and mTLS credentials are used together, not as alternatives.

Schema Definition

The TypeScript SDK uses a custom schema builder s (similar to Zod) to define input and output schemas. Each field can be annotated with additional information:

  • .describe(description) - describes the field
  • .required() - marks the field as mandatory (enforced by sixpack)
  • .optional() - marks the field as optional
  • .nullDescription(description) - describes what happens when the value is not provided

There is currently no schema-level .default(...) API. If an optional field is filled at runtime when omitted, use .nullDescription(...) to explain that fallback behavior to UI users. Use .describe(...) only for the general meaning of the field.

Available schema types:

  • s.string() - text input with possible null value
  • s.number() - number input with possible null value
  • s.boolean() - 3-state checkbox (true, false, null)
  • s.select("option1", "option2", "option3", ...) - select input with the given options and possible null value

Here is an example of the previous invoice generator with all possible annotations:

import { Supplier } from 'sixpack-sdk'
import { defineGeneratorItem, s } from 'sixpack-sdk/item'

const inputSchema = {
language: s.string().describe('Language of the invoice').required(),
amountToBill: s
.number()
.describe('Amount on the invoice')
.nullDescription('100 EUR')
.optional(),
extendedTerm: s
.boolean()
.describe('Should term be extended')
.optional(),
vatId: s.string().optional(), // this field will remain without any additional information
}

type InvoiceRequest = s.infer<typeof inputSchema>

const outputSchema = {
invoiceId: s.string(),
}

function generate(input: InvoiceRequest): s.infer<typeof outputSchema> {
const randomId = String(Date.now() % 1000)
return { invoiceId: randomId }
}

const invoiceGenerator = defineGeneratorItem({
generate,
metadata: {
name: 'Invoice Generator',
description: 'Generates invoices in the billing system and returns the invoice id',
maintainer: 'Billing team',
reportIssueEmail: 'developer-billing@sixpack.dev',
reportIssueUrl: 'https://bugtracking.com/billing/reportIssue',
inputSchema,
outputSchema,
},
})

The input must be a flat object, but the output can also be a list of objects or list of lists of objects. For all three cases, the outputSchema remains the same, describing only the flat object. Field outputKind describes the shape of the returned object and defaults to FLAT when omitted:

  • FLAT: Flat object is returned
  • LIST: List of objects defined by outputSchema is returned
  • LIST_OF_LISTS: List of lists of objects defined by outputSchema is returned

Bootstrap the supplier

To bootstrap the supplier and all declared generators:

import { Supplier } from 'sixpack-sdk'

async function main() {
const supplier = new Supplier({
name: 'BillingSupplier',
reportIssueEmail: 'developer@sixpack.dev',
})
.withGenerators(invoiceGenerator)
.withRunMode('DEPLOYMENT') // recommended for stocked templates and deployed suppliers
.withSixpackUrl('gen.sixpack.dev:443')
.withOrganization('my-organization.sixpack.dev')
.withEnvironment('TEST')
.withAuthToken('your-auth-token')
.withClientCertificatePath('./config/generator.pem')
.withClientKeyPath('./config/generator.key')
.withLogLevel('DEBUG') // optional: same as setting SIXPACK_LOG_LEVEL=DEBUG
.withVerbose(true) // optional: show low-level Temporal/Rust runtime logs too

await supplier.bootstrap()
}

main().catch(console.error)

Configuration Methods

The Supplier class provides fluent configuration methods:

MethodDescription
withSixpackUrl(url)Sets the Sixpack platform URL
withOrganization(organization)Sets the organization
withEnvironment(environment)Sets the target environment
withAuthToken(token)Sets the authentication token
withRunMode(runMode)Sets DEV or DEPLOYMENT explicitly and overrides SIXPACK_RUN_MODE
withLogLevel(logLevel)Sets the Sixpack SDK log level to DEBUG, INFO, WARN, or ERROR and overrides SIXPACK_LOG_LEVEL
withClientCertificatePath(path)Sets the path to the client certificate
withClientKeyPath(path)Sets the path to the client private key
withVerbose(verbose)Enables verbose low-level Temporal and Rust runtime logging (equivalent to setting SIXPACK_VERBOSE=true)
withGenerators(...items)Registers generator items
withOrchestrators(...items)Registers orchestrator items
withStatusListener(listener)Registers a supplier status listener callback
withRootCAPath(path)When using sixpack from website other than app.sixpack.dev, root cert path must be provided

Supplier lifecycle status events

You can subscribe to supplier lifecycle status changes by registering a listener with withStatusListener(...). The SDK currently emits CONNECTED once the supplier is connected and workers are started.

import { Supplier, SupplierStatusEvent } from 'sixpack-sdk'

function onSupplierStatusChanged(event: SupplierStatusEvent) {
console.log(`Supplier state changed to: ${event.type}`)
}

async function main() {
const supplier = new Supplier({
name: 'BillingSupplier',
reportIssueEmail: 'developer@sixpack.dev',
})
.withGenerators(invoiceGenerator)
.withStatusListener(onSupplierStatusChanged)

await supplier.bootstrap()
}

Log level

By default, the SDK log level is DEBUG.

You can override it in either of these ways:

export SIXPACK_LOG_LEVEL=WARN
const supplier = new Supplier({
name: 'BillingSupplier',
reportIssueEmail: 'developer@sixpack.dev',
}).withLogLevel('WARN')

logLevel controls Sixpack SDK application logs only.

Verbose logging

You can enable verbose logging in either of these ways:

export SIXPACK_VERBOSE=true
const supplier = new Supplier({
name: 'BillingSupplier',
reportIssueEmail: 'developer@sixpack.dev',
}).withVerbose(true)

verbose is separate from logLevel. It controls low-level Temporal and Rust runtime chatter. Keep verbose set to false when you want SDK DEBUG logs without the extra Temporal/Core noise.

The Supplier loads values from environment variables first, and fluent setter methods (such as .withLogLevel(...) and .withVerbose(...)) can override them.

For runtime-sensitive fields such as runMode, prefer calling the setter explicitly when you load configuration from a local module or .env.local file after process startup. Otherwise the supplier constructor will keep the earlier environment-derived value.

Local config modules and .env.local

Teams often keep supplier configuration in a small repo-local module instead of exporting many shell variables manually. That is a valid pattern.

import { Supplier, type RunMode } from 'sixpack-sdk'

import { localSixpackConfig } from './sixpack/local-config'

export function createSupplier() {
return new Supplier({
name: localSixpackConfig.supplierName,
reportIssueUrl: 'https://example.com/support',
})
.withSixpackUrl(localSixpackConfig.sixpackUrl)
.withOrganization(localSixpackConfig.organization)
.withAuthToken(localSixpackConfig.authToken)
.withEnvironment(localSixpackConfig.environment)
.withRunMode(localSixpackConfig.runMode)
.withClientCertificatePath(localSixpackConfig.clientCertificatePath)
.withClientKeyPath(localSixpackConfig.clientKeyPath)
}

If you type runMode in a local config module, import RunMode from the main SDK entry point:

import type { RunMode } from 'sixpack-sdk'

Configuration precedence is:

  • environment variables are read first in the constructor
  • fluent setters override those values afterwards

That means .withRunMode(...) is the safest choice when run mode comes from repo-local config rather than a shell environment.

The same pattern works well for repo-local cert and key paths. Resolve them in your local config module, then apply them with .withClientCertificatePath(...) and .withClientKeyPath(...) so the supplier uses the exact files you intended.

When you use client certificates, make sure SIXPACK_ACCOUNT, the certificate, and the token all belong to the same account shown in the Sixpack account configuration UI.

For local-first supplier projects, an .env-backed starter shape is a good default:

  • sixpack/.env
  • sixpack/config.ts
  • sixpack/supplier.ts
  • package.json scripts for sixpack:validate and sixpack:start

This is currently the most repeatable local setup pattern for teams that want a committed supplier bootstrap without relying on manual shell exports.

Validate before bootstrap

If you want to fail early on missing configuration or missing supplier metadata, call supplier.validate() before bootstrap().

const supplier = createSupplier().withGenerators(invoiceGenerator)

supplier.validate()
await supplier.bootstrap()

Detailed description of a generator

Generator inputs and outputs

Any generator input is defined using a schema with fields that must be of type s.string(), s.number(), or s.boolean(). The inputs define how the specification form will look on the UI and what inputs will be accepted on the REST interface. Types are mapped as follows:

  • s.string() - text input with possible null value
  • s.number() - number input with possible null value
  • s.boolean() - 3-state checkbox (true, false, null)
  • s.select("option1", "option2", "option3", ...) - select input with the given options and possible null value

Any generator output can be:

  • A single object (outputKind: 'FLAT')
  • A list of objects (outputKind: 'LIST')
  • A nested list of objects (outputKind: 'LIST_OF_LISTS')

The generate function

The generate function is mandatory. It performs the required operation to produce a dataset and returns necessary information, usually just a subset of the dataset. It should handle properly null/undefined fields in the input by e.g. generating random values as per required business logic.

It must:

  • Be a function that takes an input object matching the input schema
  • Return a value matching metadata.outputKind and metadata.outputSchema (or a Promise)
    • FLAT -> object
    • LIST -> object array
    • LIST_OF_LISTS -> object array of arrays
  • Complete within 30s

It may have:

  • A second argument of type Context for accessing iteration info and contextual data

Context is Sixpack's runtime object passed into generate(...) as the optional second argument. Use it when your generator needs information about the current runtime execution rather than just the input. In practice this usually means reading built-in metadata such as the current iteration number, environment, or dataset ID, and persisting custom values so the next iteration can continue where the previous one stopped.

This is Sixpack runtime context, not React context and not any other frontend framework context API.

Iterative generation

The generate function can access a Context object as a second parameter to implement iterative patterns:

import { GeneratorItem, Context, s } from 'sixpack-sdk/item'
import { IterateRequest } from 'sixpack-sdk/item'

const inputSchema = {
loanAmount: s.number().required(),
}

const outputSchema = {
loanId: s.string(),
status: s.string(),
}

function generate(
input: s.infer<typeof inputSchema>,
context: Context
): s.infer<typeof outputSchema> {
if (context.iteration === 0) {
// First iteration: do initial setup
context['someKey'] = 'myValue'
throw new IterateRequest(10 * 60 * 1000) // Wait 10 minutes
}

const myValue = context['someKey']
// ... some business logic
if (!dataReady) {
throw new IterateRequest(5 * 60 * 1000) // Wait 5 more minutes
}

return { loanId: '123', status: 'approved' }
}

The Context interface provides:

  • readonly iteration: number - the current iteration index (starts at 0)
  • readonly environment: string - the current environment
  • readonly datasetId: string - the dataset ID
  • [key: string]: unknown - arbitrary contextual data persisted between iterations

Treat the built-in fields as Sixpack-managed runtime state. Persist only your own iteration state under custom keys such as context['pollStartedAt'] or context['externalRequestId'].

Custom context values are persisted directly on Context and are available on the next iteration, including after IterateRequest and failed attempts.

Plain generator Context is mainly for runtime metadata and persisted custom values. Additional helper methods are only available on runtime shapes that explicitly include them, such as OrchestratorContext, which adds request* helpers.

Templates

Templates can be defined to specify input combinations that should be pre-generated and maintained in stock:

import { defineGeneratorItem, s } from 'sixpack-sdk/item'

const inputSchema = {
language: s.string().required(),
amountToBill: s.number().optional(),
extendedTerm: s.boolean().optional(),
}

const invoiceGenerator = defineGeneratorItem({
generate,
metadata: {
name: 'Invoice',
inputSchema,
outputSchema,
reportIssueEmail: 'developer@sixpack.dev',
},
templates: [
{ input: { language: 'English', amountToBill: 100 }, minimum: 10 },
{ input: { language: 'French' }, minimum: 5 },
// Optional schema fields may be omitted from template input objects.
{ input: { language: 'Italian', extendedTerm: true }, minimum: 2 },
],
})

// These fail during TypeScript compilation:
// { input: { language: 123 }, minimum: 1 }
// { input: { language: 'English', unexpected: true }, minimum: 1 }

This will tell Sixpack to always try to maintain at least 10 invoices in English and 5 invoices in French in stock. Use defineGeneratorItem(...) or defineOrchestratorItem(...) when you want template inputs inferred from inputSchema. With plain GeneratorItem or OrchestratorItem annotations, that stronger templates[].input inference is not available.

Important:

  • stock maintenance happens in DEPLOYMENT, not DEV
  • template inputs should describe stable request shapes, not hard-coded unique business identities

For unique entities such as usernames, emails, or customer ids, prefer optional input fields plus runtime-generated unique defaults inside generate(...). Keep the returned output flat so callers receive the generated values directly.

When you do this, add a UI-facing nullDescription(...) so users can see what happens when they leave the field empty. For example:

const inputSchema = {
username: s
.string()
.nullDescription('Auto-generated unique username')
.optional(),
password: s
.string()
.nullDescription('Default password chosen by the generator')
.optional(),
}

That makes the generated behavior visible to users in the UI instead of hiding it only in generator code.

If templates are not defined, Sixpack will compute templates automatically by creating combinations of fields and will try to maintain at least 1 for each combination. If you would like not to pre-generate datasets, return an empty array.

Orchestrator

To combine or chain the output of generators, use an orchestrator. It behaves almost the same as a generator with following differences:

  • Must be deterministic
  • Can use the context.request() method to request data from other generators
  • Orchestrator must not import Supplier from 'sixpack-sdk'
  • Orchestrator must not import certain packages such as fs, path, etc.

Bootstrap with an orchestrator

// supplier.ts
import { Supplier } from 'sixpack-sdk'
import { invoiceOrchestrator } from './orchestrator'
import { customerGenerator } from './generators'

async function main() {
const supplier = new Supplier({
name: 'Billing',
description: 'Billing system supplier',
maintainer: 'John Doe',
reportIssueEmail: 'dev@sixpack.dev',
})
.withOrchestrators(invoiceOrchestrator)
.withGenerators(customerGenerator)

await supplier.bootstrap()
}

main().catch(console.error)
// orchestrator.ts
import {
defineOrchestratorItem,
OrchestratorContext,
s,
} from 'sixpack-sdk/item'

const inputSchema = {
language: s.string().required(),
amountToBill: s.number().optional(),
extendedTerm: s.boolean().optional(),
vatId: s.string().optional(),
}

type InvoiceRequest = s.infer<typeof inputSchema>

const outputSchema = {
customerId: s.string(),
invoiceId: s.string(),
}

// The generate function must be exported
export async function generate(
input: InvoiceRequest,
context: OrchestratorContext
): Promise<s.infer<typeof outputSchema>> {
// Request customer from another generator
const customer = await context.request(
'Billing', // supplier name
'CustomerGenerator', // item name
{ language: input.language } // input for the generator
)

const randomId = String(Date.now() % 1000)
return {
customerId: customer.customerId,
invoiceId: randomId,
}
}

export const invoiceOrchestrator = defineOrchestratorItem({
// When using CommonJS Modules:
generatePath: __filename,
// Or alternative when using ESM: (import { urlToPath } from "sixpack-sdk/item";).
// generatePath: fileURLToPath(import.meta.url),
// make sure you are not using node:url package, as imports from node: are not supported.
metadata: {
name: 'Invoice Orchestrator',
inputSchema,
outputSchema,
outputKind: 'FLAT',
reportIssueEmail: 'dev@sixpack.dev',
description: 'Orchestrates invoice generation with customer data',
},
templates: [
{ input: { language: 'English' }, minimum: 5 },
],
})

The context.request() method behaves similarly to dataset request on the UI or via REST - if the requested dataset is in stock, it's returned immediately, otherwise it is requested ad-hoc to the corresponding generator or orchestrator. Orchestrators can obtain data from other orchestrators, making it easy to compose complex datasets using simple native code.

When creating orchestrators, sixpack uses webpack to bundle the orchestrator code and runs it in a sandboxed environment.

Choosing the correct request method

When an orchestrator requests data from another item, the context method must match the requested item's output kind.

Use this mapping:

  • context.request(...) for items returning a single object (outputKind: 'FLAT')
  • context.requestList(...) for items returning a list of objects (outputKind: 'LIST')
  • context.requestListOfLists(...) for items returning a list of lists (outputKind: 'LIST_OF_LISTS')
  • context.requestMany(...) for collection input (Configuration[]) when the child item returns outputKind: 'FLAT'
  • context.requestManyLists(...) for collection input (Configuration[]) when the child item returns outputKind: 'LIST'

When using collection input (context.requestMany*), output kind is promoted by one level:

  • Child FLAT -> parent receives LIST (use context.requestMany(...))
  • Child LIST -> parent receives LIST_OF_LISTS (use context.requestManyLists(...))
  • Child LIST_OF_LISTS -> not supported for collection input (context.requestMany* throws an error)

Both context.requestMany(...) and context.requestManyLists(...) take a collection of input configurations. Conceptually, Sixpack performs one child request per collection entry and aggregates the results in the returned collection shape. This means the item is called n-times where n is the number of inputs.

If you use the wrong method for the target output kind, the request fails at runtime.

Exceptions

For common retry and failure behaviour, see Exception handling.

TypeScript exception classes:

ClassThrown byUse
IterateRequestYour generator codeWait and run the same dataset again after a delay
SixpackNonRetriableExceptionYour generator or orchestrator codeMark the current dataset as failed without retrying it as-is
ObtainFailedExceptionOrchestrator context.request* methodsReport that a requested child dataset could not be obtained

Imports:

import { IterateRequest, SixpackNonRetriableException, ObtainFailedException } from 'sixpack-sdk/item'

Halt and retry after 5 minutes:

throw new IterateRequest(5 * 60 * 1000, 'Waiting for approval')

Throw a non-retryable failure from a generator:

if (await emailAlreadyExists(input.email)) {
throw new SixpackNonRetriableException(
`Customer email already exists: ${input.email}`
)
}

Handle a failed child request in an orchestrator:

try {
const account = await context.request<{ id: string }>(
'Core Banking',
'Account',
customer
)

return {
customerId: customer.id,
accountId: account.id,
}
} catch (error) {
if (error instanceof ObtainFailedException) {
throw new SixpackNonRetriableException(
`Unable to create account for generated customer ${customer.id}`
)
}

throw error
}

If context.request(...), context.requestList(...), context.requestListOfLists(...), context.requestMany(...), or context.requestManyLists(...) cannot obtain the requested child dataset, it throws ObtainFailedException.

If the supplier name or item name passed to a request method is null, the SDK throws SixpackNonRetriableException.

Custom Logger

You can provide a custom logger implementation:

import { Logger, setLogger } from 'sixpack-sdk/logger'

class CustomLogger implements Logger {
debug(message: string): void {
console.debug(`[DEBUG] ${message}`)
}

info(message: string): void {
console.info(`[INFO] ${message}`)
}

warn(message: string): void {
console.warn(`[WARN] ${message}`)
}

error(message: string): void {
console.error(`[ERROR] ${message}`)
}
}

// Set the custom logger before creating the supplier
setLogger(new CustomLogger())

You can also use the default logger directly:

import { logger } from 'sixpack-sdk/logger'

logger.info('Starting generator...')
logger.error('Something went wrong')

Self hosting

If you are self hosting sixpack, (or using instance other than app.sixpack.dev), you will need to provide root certificate (as well as the other two certificates):


async function main() {
const supplier = new Supplier({
name: 'BillingSupplier',
reportIssueEmail: 'developer@sixpack.dev',
})
.withGenerators(invoiceGenerator)
.withSixpackUrl('gen.sixpack.dev:443')
.withOrganization('my-organization.sixpack.dev')
.withEnvironment('TEST')
.withAuthToken('your-auth-token')
.withClientCertificatePath('./config/generator.pem')
.withClientKeyPath('./config/generator.key')
.withRootCAPath('./config/ca.pem') // this line needs to be added

await supplier.bootstrap()
}

main().catch(console.error)

You can download all three certificates in settings tab on UI on self hosted instances.

Common issues

__filename and fileURLToPath(import.meta.url) issues

If __filename or import.meta.url are not recognized, please make sure you are using the correct term for your target module system. CommonJS environments use __filename and __dirname, while ES Modules (ESM) rely on import.meta.url. You can verify your current transpilation target by checking the "module" setting in your tsconfig.json file, as well as the "type" field in your package.json.