Skip to main content

Soln SDK

Use studioScriptContext when a Studio script needs to read or write records, inspect form metadata, create file assets, query organization directory data, interact with approval runtime data, or send a message from a mounted workspace app.

This SDK is available in Studio scripts configured from places such as Button Design, Layout Design, approval service calls, and integration flows.

Basic Usage​

export default async function main(payload: StudioScriptRuntimeContextPayload, context: StudioScriptContext) {
const created = await context.form.create({
formApiName: 'orders',
title: 'SO-1001',
data: {
orderNo: 'SO-1001',
amount: 1200,
},
});

return {
ok: true,
recordId: created.id,
};
}

payload always uses soln.runtime-context/v1; caller-provided values are under payload.input. Every remote SDK method accepts its documented input as one object argument; methods whose input is optional may be called without arguments. Never pass positional or multiple arguments. Use formApiName for APIs that target a form, and recordId for APIs that target an existing record. For example, update with context.form.update({ recordId, data }). Do not hard-code form IDs in scripts.

SDK responses format form data by field metadata. date fields are returned as YYYY-MM-DD, and datetime fields are returned as YYYY-MM-DD HH:mm:ss.

Every paginated SDK method uses cursor pagination only. Omit cursor for the first request, pass the returned nextCursor to the next request, and keep the remaining query conditions and limit unchanged. Legacy page and pageSize inputs are not accepted. A response is complete when hasMore is false and nextCursor is null.

Client​

interface StudioScriptContext {
runtime: StudioScriptRuntimeInfo;
logger: StudioScriptLogger;
http: StudioScriptHttpClient;
file: StudioScriptFileClient;
form: StudioScriptFormClient;
formSavedFilter: StudioScriptFormSavedFilterClient;
formSchema: StudioScriptFormSchemaClient;
formTableView: StudioScriptFormTableViewClient;
approval: StudioScriptApprovalClient;
organization: StudioScriptOrganizationClient;
department: StudioScriptDepartmentClient;
member: StudioScriptMemberClient;
iam: StudioScriptIamClient;
}

type StudioScriptRuntimeInfo = {
tenantId: string;
assetId: string;
assetCode: string;
workspaceAppCode: string | null;
executionId: string;
startedAt: string;
traceId: string;
};

type StudioScriptLogger = {
log: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
debug: (...args: unknown[]) => void;
};

interface StudioScriptHttpClient {
request(input: StudioScriptHttpRequestInput): Promise<StudioScriptHttpResponse>;
requestJson<TData = unknown>(input: StudioScriptHttpRequestInput): Promise<StudioScriptHttpJsonResponse<TData>>;
}

interface StudioScriptFormClient {
list(input: StudioScriptFormListInput): Promise<StudioScriptFormRecordList>;
find(input: StudioScriptFormFindInput): Promise<StudioScriptFormRecord | null>;
get(input: { recordId: string }): Promise<StudioScriptFormRecord | null>;
batchGet(input: { recordIds: string[] }): Promise<StudioScriptFormBatchGetResult>;
create(input: StudioScriptFormCreateInput): Promise<StudioScriptFormRecord | StudioScriptFormCreateResult>;
createWithChildTables(input: StudioScriptFormCreateWithChildTablesInput): Promise<StudioScriptFormCreateWithChildTablesResult>;
batchCreate(input: StudioScriptFormBatchCreateInput): Promise<StudioScriptFormBatchCreateResult>;
batchCreateWithChildTables(input: StudioScriptFormBatchCreateWithChildTablesInput): Promise<StudioScriptFormBatchCreateWithChildTablesResult>;
batchUpdate(input: StudioScriptFormBatchUpdateInput): Promise<StudioScriptFormBatchUpdateResult>;
updateWithChildTables(input: StudioScriptFormUpdateWithChildTablesInput): Promise<StudioScriptFormUpdateWithChildTablesResult | null>;
batchUpdateWithChildTables(input: StudioScriptFormBatchUpdateWithChildTablesInput): Promise<StudioScriptFormBatchUpdateWithChildTablesResult>;
update(input: StudioScriptFormUpdateInput): Promise<StudioScriptFormRecord | null>;
trash(input: StudioScriptFormDeleteInput): Promise<{ trashed: boolean }>;
batchTrash(input: StudioScriptFormBatchDeleteInput): Promise<StudioScriptFormBatchTrashResult>;
restore(input: StudioScriptFormRestoreInput): Promise<StudioScriptFormRecord | null>;
batchDelete(input: StudioScriptFormBatchDeleteInput): Promise<StudioScriptFormBatchDeleteResult>;
delete(input: StudioScriptFormDeleteInput): Promise<{ deleted: boolean }>;
}

interface StudioScriptFormRecordList {
items: StudioScriptFormRecord[];
limit: number;
projectionAsOf: string | null;
hasMore: boolean;
nextCursor: string | null;
}

interface StudioScriptFormSavedFilterClient {
list(input: { formApiName: string }): Promise<{ items: StudioScriptFormSavedFilter[] }>;
save(input: { formApiName: string; id?: string | null; name: string; condition: Record<string, unknown> }): Promise<StudioScriptFormSavedFilter>;
delete(input: { filterId: string }): Promise<{ id: string }>;
}

interface StudioScriptFormTableViewClient {
list(input: { formApiName: string }): Promise<{ items: StudioScriptFormTableView[] }>;
save(input: {
formApiName: string;
id?: string | null;
name: string;
layoutIds?: string[] | null;
groupFieldIds?: string[] | null;
scopes?: string[] | null;
bordered?: boolean | null;
showRowNumber?: boolean | null;
groups?: StudioScriptFormTableViewGroup[] | null;
}): Promise<StudioScriptFormTableView>;
delete(input: { viewId: string }): Promise<{ id: string }>;
}

interface StudioScriptFormSavedFilter {
id: string;
tenantId: string;
formId: string;
ownerType: string;
ownerId: string;
name: string;
condition: unknown;
sortOrder: number;
createdAt: string;
updatedAt: string;
}

interface StudioScriptFormTableViewGroup {
id: string;
name: string;
fieldIds: string[];
level: 1 | 2 | 3;
parentId: string | null;
}

interface StudioScriptFormTableView {
id: string;
tenantId: string;
formId: string;
ownerType: string;
ownerId: string;
name: string;
layoutIds: string[];
groupFieldIds: string[];
scopes: string[];
bordered: boolean;
showRowNumber: boolean;
groups: StudioScriptFormTableViewGroup[];
sortOrder: number;
createdAt: string;
updatedAt: string;
}

Context clients:

ClientTypeDescription
runtimeStudioScriptRuntimeInfoCurrent execution metadata, including tenant, asset, verified workspace app, execution, and trace IDs
loggerStudioScriptLoggerWrites structured execution logs visible in Studio run output
httpStudioScriptHttpClientSends outbound HTTP requests from server-side Studio runtime
fileStudioScriptFileClientResolves and creates file assets
formStudioScriptFormClientReads and writes form records
formSavedFilterStudioScriptFormSavedFilterClientReads and manages saved filters for tenant forms
formSchemaStudioScriptFormSchemaClientReads form and field metadata
formTableViewStudioScriptFormTableViewClientReads and manages table views for tenant forms
approvalStudioScriptApprovalClientSubmits records into approval and handles approval runtime data
organizationStudioScriptOrganizationClientReads organization profile form data and organization tree
departmentStudioScriptDepartmentClientReads department profile form data and department tree
memberStudioScriptMemberClientReads member profile form data
iamStudioScriptIamClientReads current or specified member role summaries

Form client methods:

MethodInputReturnDescription
listStudioScriptFormListInputStudioScriptFormRecordListGets records in batches by query conditions
findStudioScriptFormFindInputStudioScriptFormRecord | nullSingle-record query by conditions
get{ recordId: string }StudioScriptFormRecord | nullGets one record by record ID
batchGet{ recordIds: string[] }{ items: StudioScriptFormRecord[]; total: number }Gets multiple records by record IDs
createStudioScriptFormCreateInputStudioScriptFormRecord | StudioScriptFormCreateResultCreates one form record and can optionally submit it into approval
createWithChildTablesStudioScriptFormCreateWithChildTablesInputStudioScriptFormCreateWithChildTablesResultCreates one parent record and child-table records in one transaction
batchCreateStudioScriptFormBatchCreateInputStudioScriptFormBatchCreateResultCreates multiple form records in one transaction
batchCreateWithChildTablesStudioScriptFormBatchCreateWithChildTablesInputStudioScriptFormBatchCreateWithChildTablesResultCreates multiple parent records and their child-table records in one transaction
batchUpdateStudioScriptFormBatchUpdateInputStudioScriptFormBatchUpdateResultUpdates multiple form records in one transaction
updateWithChildTablesStudioScriptFormUpdateWithChildTablesInputStudioScriptFormUpdateWithChildTablesResult | nullUpdates one parent record and child-table records in one transaction
batchUpdateWithChildTablesStudioScriptFormBatchUpdateWithChildTablesInputStudioScriptFormBatchUpdateWithChildTablesResultUpdates multiple parent records and their child-table records in one transaction
updateStudioScriptFormUpdateInputStudioScriptFormRecord | nullUpdates one form record
trashStudioScriptFormDeleteInput{ trashed: boolean }Moves one form record to the trash
batchTrashStudioScriptFormBatchDeleteInputStudioScriptFormBatchTrashResultMoves multiple form records to the trash
restoreStudioScriptFormRestoreInputStudioScriptFormRecord | nullRestores one trashed form record and may update title/data/status as part of the restore
batchDeleteStudioScriptFormBatchDeleteInputStudioScriptFormBatchDeleteResultPermanently deletes multiple form records
deleteStudioScriptFormDeleteInput{ deleted: boolean }Permanently deletes one form record

trash is the default business delete operation. It moves a record to the trash and can usually be restored by product flows. delete is a hard delete and should be used only when the business script intentionally removes the record permanently.

Saved filter and table view methods:

ClientMethodInputReturnDescription
formSavedFilterlist{ formApiName: string }{ items: StudioScriptFormSavedFilter[] }Lists saved filters for one form
formSavedFiltersave{ formApiName: string; id?: string; name: string; condition: object }StudioScriptFormSavedFilterCreates or updates one saved filter
formSavedFilterdelete{ filterId: string }{ id: string }Deletes one saved filter
formTableViewlist{ formApiName: string }{ items: StudioScriptFormTableView[] }Lists table views for one form
formTableViewsave{ formApiName: string; id?: string; name: string; layoutIds?; groupFieldIds?; scopes?; bordered?; showRowNumber?; groups? }StudioScriptFormTableViewCreates or updates one table view. There is no columns input
formTableViewdelete{ viewId: string }{ id: string }Deletes one table view

Runtime, Logger, And HTTP​

Use context.runtime for execution identifiers, context.logger for logs, and context.http for outbound server-side requests.

export default async function main(payload: StudioScriptRuntimeContextPayload, context: StudioScriptContext) {
context.logger.info('execution', context.runtime.executionId);

const response = await context.http.requestJson({
url: 'https://example.com/api/orders',
method: 'POST',
json: payload,
timeoutMs: 10000,
retry: {
maxAttempts: 2,
},
});

return {
ok: response.ok,
status: response.status,
data: response.data,
traceId: context.runtime.traceId,
};
}

HTTP request input:

type StudioScriptHttpRequestInput = {
url: string;
method?: string;
query?: Record<string, unknown>;
headers?: Record<string, unknown>;
json?: unknown;
body?: unknown;
timeoutMs?: number;
retry?: number | {
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
factor?: number;
retryableStatusCodes?: number[];
};
expect?: 'json' | 'text';
throwOnHttpError?: boolean;
idempotency?: boolean | string | {
key?: string;
headerName?: string;
};
};

type StudioScriptHttpResponse = {
ok: boolean;
status: number;
code: string;
url: string;
method: string;
headers: Record<string, string>;
requestId: string;
traceId: string | null;
idempotencyKey: string | null;
durationMs: number;
attempts: number;
bodyText: string;
bodyJson: unknown;
};

type StudioScriptHttpJsonResponse<TData = unknown> = StudioScriptHttpResponse & {
data: TData | null;
};

requestJson is a convenience wrapper that sets expect: 'json' and returns data as the parsed JSON body.

Choose a logger method by severity:

MethodPurpose
context.logger.debugTemporary execution diagnostics
context.logger.info / logNormal execution stages and identifiers
context.logger.warnRecoverable or non-blocking abnormal states
context.logger.errorFailed operations or operations about to return failure

Do not log secrets, Authorization headers, complete personal profiles, or file contents.

HTTP rules:

  • Use context.http in server scripts, not direct fetch
  • Set a bounded timeoutMs for every external request
  • Prefer requestJson for JSON APIs and request for text or custom responses
  • Set throwOnHttpError: false and inspect response.ok when the script handles 4xx/5xx itself
  • Use a stable idempotency key for external writes; approval and automation scripts can normally use context.runtime.traceId
  • Do not hard-code credentials in scripts; configure external authentication in the OpenLink integration system

Record Shape​

type StudioScriptFormRecord = {
id: string;
tenantId: string;
formId: string;
formName: string;
title: string;
data: Record<string, unknown>;
dataStatus: 'draft' | 'active' | 'archived' | 'trashed';
approvalStatus: 'none' | 'pending' | 'approved' | 'rejected' | 'returned' | 'canceled';
approvalFlowInstanceId: string | null;
createdAt: string;
updatedAt: string;
createdBy?: StudioScriptFormRecordPrincipal;
updatedBy?: StudioScriptFormRecordPrincipal;
};

type StudioScriptFormCreateResult = StudioScriptFormRecord & {
approval: {
queued: true;
submitted: false;
jobId: string;
action: 'create';
record: {
recordId: string;
formId: string;
};
};
};

type StudioScriptFormBatchCreateResult = {
items: Array<StudioScriptFormRecord | StudioScriptFormCreateResult>;
total: number;
};

type StudioScriptFormCreateWithChildTablesResult = {
record: StudioScriptFormRecord | StudioScriptFormCreateResult;
childTables: Array<{
childFormApiName: string;
items: Array<StudioScriptFormRecord | StudioScriptFormCreateResult>;
total: number;
}>;
};

type StudioScriptFormBatchCreateWithChildTablesResult = {
items: StudioScriptFormCreateWithChildTablesResult[];
total: number;
};

type StudioScriptFormRecordPrincipal = {
memberId?: string | null;
departmentId?: string | null;
organizationId?: string | null;
};

Record field values are stored in data and keyed by field API name.

Return And Failure Behavior​

Form SDK methods resolve with the documented return type when the operation succeeds. They do not return { ok: false } for business or permission errors.

MethodSuccess returnEmpty or missing targetFailure behavior
listStudioScriptFormRecordListitems: [], hasMore: false, nextCursor: nullPromise rejects
findStudioScriptFormRecordnullPromise rejects
createStudioScriptFormRecord or StudioScriptFormCreateResultNot applicablePromise rejects
createWithChildTablesStudioScriptFormCreateWithChildTablesResultNot applicablePromise rejects
batchCreateStudioScriptFormBatchCreateResultNot applicablePromise rejects
batchCreateWithChildTablesStudioScriptFormBatchCreateWithChildTablesResultNot applicablePromise rejects
batchUpdateStudioScriptFormBatchUpdateResultNot applicablePromise rejects
updateWithChildTablesStudioScriptFormUpdateWithChildTablesResultnull when the target parent record is not foundPromise rejects
batchUpdateWithChildTablesStudioScriptFormBatchUpdateWithChildTablesResultNot applicablePromise rejects
updateStudioScriptFormRecordnull when the target record is not foundPromise rejects
trash{ trashed: boolean }{ trashed: false } when the target record is not foundPromise rejects
batchTrashStudioScriptFormBatchTrashResultMissing targets are reported in failedPromise rejects only for invalid input or runtime-level failures
batchDeleteStudioScriptFormBatchDeleteResultMissing targets are reported in failedPromise rejects only for invalid input or runtime-level failures
delete{ deleted: boolean }{ deleted: false } when the target record is not foundPromise rejects

When a form SDK call fails, it throws an Error inside the script worker. If the script does not catch it, the whole script execution fails with STUDIO_ASSET_EXECUTION_FAILED, and the execution result's errorMessage is the underlying error message such as IAM_PERMISSION_DENIED or FORMS_SCHEMA_NOT_FOUND.

Use try/catch when the script wants to return a controlled result:

export default async function main(payload: StudioScriptRuntimeContextPayload, context: StudioScriptContext) {
try {
const record = await context.form.create({
formApiName: 'orders',
title: 'SO-1002',
data: {
orderNo: 'SO-1002',
},
});

return {
ok: true,
record,
};
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
}

Record Query​

Find First Matching Record​

const record = await context.form.find({
formApiName: 'orders',
sortFieldApiName: 'order_amount',
sortDirection: 'DESC',
condition: {
logic: 'and',
children: [
{
field: 'so_order_no',
operator: 'eq',
value: 'SO-1001',
},
],
},
});

Use find with query conditions when you need the first matching record. It uses the same filters as list and always includes detailed record data.

When you already have a record ID, filter by recordId:

const record = await context.form.find({
formApiName: 'orders',
condition: {
logic: 'and',
children: [
{
field: 'recordId',
operator: 'eq',
value: 'record-id',
},
],
},
});

Condition query input:

type StudioScriptFormFindInput = Omit<
StudioScriptFormListInput,
'cursor' | 'limit' | 'includeDetail'
>;
const record = await context.form.find({
formApiName: 'orders',
condition: {
logic: 'and',
children: [
{
field: 'recordId',
operator: 'eq',
value: payload.record?.id,
},
],
},
});

Use recordId in conditions when you already have the target record ID. recordId matches the record's primary ID.

Use sortFieldApiName and sortDirection when multiple records may match and you need to control which record is returned first. Sorting supports number, time, date, datetime, week, month, quarter, year, and serial_number fields.

Parameters:

ParameterTypeRequiredDescription
condition.children[].fieldstringYesUse recordId to filter by target form record ID
condition.children[].operator'eq' | 'in'YesUse eq for one record ID or in for multiple record IDs
condition.children[].valuestring | string[]YesTarget form record ID value or values
sortFieldApiNamestring | nullNoField API name used for sorting before returning the first matched record
sortDirection'ASC' | 'DESC'NoSort direction. Defaults to DESC

Get Records By Conditions​

const records = await context.form.list({
formApiName: 'orders',
limit: 20,
keyword: 'SO-1001',
includeDetail: true,
});

Input:

type StudioScriptFormListInput = {
formApiName: string;
cursor?: string | null;
limit?: number;
keyword?: string | null;
keywords?: string[] | null;
includeDetail?: boolean;
sortFieldApiName?: string | null;
sortDirection?: 'ASC' | 'DESC' | 'asc' | 'desc' | null;
searchFieldApiName?: string | null;
dataStatuses?: Array<'draft' | 'active' | 'archived' | 'trashed'>;
approvalStatuses?: Array<'none' | 'pending' | 'approved' | 'rejected' | 'returned' | 'canceled'>;
visibilityStates?: Array<{
dataStatus: 'draft' | 'active' | 'archived' | 'trashed';
approvalStatus: 'none' | 'pending' | 'approved' | 'rejected' | 'returned' | 'canceled';
}>;
condition?: StudioScriptFormConditionGroup | null;
};

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name. The runtime resolves it to the tenant-side form ID
cursorstring | nullNoOpaque nextCursor returned by the previous call. Keep the remaining query conditions unchanged
limitnumberNoMaximum number of records to return. Defaults to 20; maximum is 200
keywordstring | nullNoSingle keyword for runtime search
keywordsstring[] | nullNoMultiple keywords for runtime search
includeDetailbooleanNoWhether list rows should include detailed record data. Defaults to false
sortFieldApiNamestring | nullNoField API name used for sorting. Supports number, time, date, datetime, week, month, quarter, year, and serial_number fields
sortDirection'ASC' | 'DESC'NoSort direction. Defaults to DESC
searchFieldApiNamestring | nullNoField API name used as the keyword search target
dataStatusesArray<'draft' | 'active' | 'archived' | 'trashed'>NoFilters by record data status
approvalStatusesArray<'none' | 'pending' | 'approved' | 'rejected' | 'returned' | 'canceled'>NoFilters by approval status
visibilityStatesArray<object>NoFilters by combined data status and approval status states
conditionStudioScriptFormConditionGroup | nullNoStructured field condition

Supported operators:

OperatorMeaning
eqEquals
neqNot equals
containsContains
not_containsDoes not contain
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal
inIn list
emptyEmpty
not_emptyNot empty

Response:

type StudioScriptFormRecordList = {
items: StudioScriptFormRecord[];
limit: number;
projectionAsOf: string | null;
hasMore: boolean;
nextCursor: string | null;
};

type StudioScriptFormBatchGetResult = {
items: StudioScriptFormRecord[];
total: number;
};

Read Records By ID​

When the record ID is already known, call get directly instead of building a recordId condition for find:

const record = await context.form.get({
recordId: payload.record?.id || '',
});

if (!record) {
return { ok: false, message: 'Record not found.' };
}

get returns the full record or null and enforces the current actor's form, row, and field permissions.

Use batchGet when several record IDs are known. One call accepts at most 100 IDs:

const result = await context.form.batchGet({
recordIds: selectedRecordIds,
});

batchGet returns only readable records. Do not assume its result has the same length as the input. Build a Map by returned record id when correspondence matters.

Traverse All Cursor Pages​

Request one page at a time with nextCursor, keeping all other query inputs unchanged:

const items: StudioScriptFormRecord[] = [];
let cursor: string | null = null;

do {
const page = await context.form.list({
formApiName: 'orders',
cursor,
limit: 200,
includeDetail: true,
dataStatuses: ['active'],
sortFieldApiName: 'orderDate',
sortDirection: 'ASC',
});

items.push(...page.items);
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);

Traverse every page only for explicitly bounded data sets. Use an OpenLink paged flow or data-processing job for large synchronization work instead of performing an unbounded read in one Studio script.

Record Creation​

Create A Single Record​

const record = await context.form.create({
formApiName: 'orders',
title: 'SO-1002',
data: {
orderNo: 'SO-1002',
status: 'draft',
},
submitApproval: true,
});

Input:

type StudioScriptFormCreateInput = {
formApiName: string;
title: string;
data: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
};

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name. The runtime resolves it to the tenant-side form ID
titlestringYesRecord title used by the runtime list and display layer
dataRecord<string, unknown>YesField values keyed by field API name. Must be an object
buttonKeystring | nullNoOptional button key passed to the runtime write chain
submitApprovalbooleanNoWhen true, creates the record and queues an approval submission job in the same transaction

When submitApproval is true, the returned record includes an approval field with the queued approval submission jobId. The background approval submission job uses action create, and the approval runtime only uses a published approval definition whose create trigger is enabled. If queueing the job fails, the SDK call rejects and the record creation is rolled back.

Batch Create Records​

const result = await context.form.batchCreate({
formApiName: 'orders',
records: [
{
title: 'SO-1003',
data: {
orderNo: 'SO-1003',
status: 'draft',
},
},
{
title: 'SO-1004',
data: {
orderNo: 'SO-1004',
status: 'draft',
},
submitApproval: true,
},
],
});

Input:

type StudioScriptFormBatchCreateInput = {
formApiName: string;
records: Array<{
title: string;
data: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
}>;
};

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name. The runtime resolves it to the tenant-side form ID
recordsArray<object>YesBatch data to create. Each batch must contain 1 to 100 records
records[].titlestringYesRecord title used by the runtime list and display layer
records[].dataRecord<string, unknown>YesField values keyed by field API name. Must be an object
records[].buttonKeystring | nullNoOptional button key passed to the runtime write chain
records[].submitApprovalbooleanNoWhen true, queues an approval submission job for that created record in the same transaction

Response:

type StudioScriptFormBatchCreateResult = {
items: Array<StudioScriptFormRecord | StudioScriptFormCreateResult>;
total: number;
};

batchCreate creates all records in one transaction. If any record fails validation, permission checks, auto-number allocation, file reference handling, formula calculation, post-create job enqueueing, or approval job enqueueing, the SDK call rejects and all records in the batch are rolled back.

Create Parent And Child Table Records​

const result = await context.form.createWithChildTables({
formApiName: 'orders',
title: 'SO-1005',
data: {
orderNo: 'SO-1005',
status: 'draft',
},
childTables: [
{
childFormApiName: 'order_items',
records: [
{
title: 'SO-1005-1',
data: {
productName: 'Keyboard',
quantity: 2,
},
},
{
title: 'SO-1005-2',
data: {
productName: 'Mouse',
quantity: 1,
},
},
],
},
],
});

Input:

type StudioScriptFormCreateWithChildTablesInput = {
formApiName: string;
title: string;
data: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
childTables: Array<{
childFormApiName: string;
records: Array<{
title: string;
data: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
}>;
}>;
};

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesParent form API name. The runtime resolves it to the tenant-side parent form ID
titlestringYesParent record title used by the runtime list and display layer
dataRecord<string, unknown>YesParent field values keyed by field API name. Must be an object
childTablesArray<object>YesChild-table batches to create. Must contain at least one child table
childTables[].childFormApiNamestringYesChild form API name. It must be a child table of the parent form
childTables[].recordsArray<object>YesChild-table batch data. Each child-table batch must contain 1 to 100 records
childTables[].records[].titlestringYesChild record title used by the runtime list and display layer
childTables[].records[].dataRecord<string, unknown>YesChild field values keyed by field API name. Do not include the parent relation field; the SDK fills it with the created parent record ID
childTables[].records[].buttonKeystring | nullNoOptional button key passed to the runtime write chain
childTables[].records[].submitApprovalbooleanNoWhen true, queues an approval submission job for that created child record in the same transaction

Response:

type StudioScriptFormCreateWithChildTablesResult = {
record: StudioScriptFormRecord | StudioScriptFormCreateResult;
childTables: Array<{
childFormApiName: string;
items: Array<StudioScriptFormRecord | StudioScriptFormCreateResult>;
total: number;
}>;
};

createWithChildTables first creates the parent record, then creates each child record with the parent relation field set to the created parent record ID. The whole operation runs in one transaction. If the parent record, any child-table lookup, any child record, post-create job enqueueing, or approval job enqueueing fails, the SDK call rejects and the whole operation is rolled back.

Batch Create Parent And Child Table Records​

const result = await context.form.batchCreateWithChildTables({
formApiName: 'orders',
records: [
{
title: 'SO-1006',
data: {
orderNo: 'SO-1006',
status: 'draft',
},
childTables: [
{
childFormApiName: 'order_items',
records: [
{
title: 'SO-1006-1',
data: {
productName: 'Keyboard',
quantity: 2,
},
},
],
},
],
},
{
title: 'SO-1007',
data: {
orderNo: 'SO-1007',
status: 'draft',
},
childTables: [
{
childFormApiName: 'order_items',
records: [
{
title: 'SO-1007-1',
data: {
productName: 'Mouse',
quantity: 1,
},
},
],
},
],
},
],
});

Input:

type StudioScriptFormBatchCreateWithChildTablesInput = {
formApiName: string;
records: Array<{
title: string;
data: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
childTables: Array<{
childFormApiName: string;
records: Array<{
title: string;
data: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
}>;
}>;
}>;
};

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesParent form API name. The runtime resolves it to the tenant-side parent form ID
recordsArray<object>YesParent batch data to create. Each batch must contain 1 to 100 parent records
records[].titlestringYesParent record title used by the runtime list and display layer
records[].dataRecord<string, unknown>YesParent field values keyed by field API name. Must be an object
records[].childTablesArray<object>YesChild-table batches for this parent record. Must contain at least one child table
records[].childTables[].childFormApiNamestringYesChild form API name. It must be a child table of the parent form
records[].childTables[].recordsArray<object>YesChild-table batch data. Each child-table batch must contain 1 to 100 records

Response:

type StudioScriptFormBatchCreateWithChildTablesResult = {
items: StudioScriptFormCreateWithChildTablesResult[];
total: number;
};

batchCreateWithChildTables is for import-style scenarios where each parent row has its own child-table rows. The whole batch runs in one transaction. If any parent record, child-table lookup, child record, post-create job enqueueing, or approval job enqueueing fails, the SDK call rejects and the whole batch is rolled back.

Update Records​

Update One Record​

const updated = await context.form.update({
recordId: record.id,
data: {
status: 'confirmed',
},
});

Input:

type StudioScriptFormUpdateInput = {
recordId: string;
title?: string;
data?: Record<string, unknown>;
buttonKey?: string | null;
submitApproval?: boolean;
};

Parameters:

ParameterTypeRequiredDescription
recordIdstringYesTarget form record ID
titlestringNoNew record title. Omit it to keep the current title
dataRecord<string, unknown>NoField values to merge into the existing record data
buttonKeystring | nullNoOptional button permission key. When provided, button execution permission is checked
submitApprovalbooleanNoWhen true, queues an approval submission job for this updated record in the same transaction

data is merged into the existing record data. Read-only calculated fields such as summary and dynamic formula fields are ignored by the existing form-data write flow.

Batch Update Records​

const result = await context.form.batchUpdate({
records: [
{
recordId: 'record-id-1',
data: {
status: 'confirmed',
},
},
{
recordId: 'record-id-2',
title: 'Updated Title',
data: {
status: 'approved',
},
submitApproval: true,
},
],
});

Input:

type StudioScriptFormBatchUpdateInput = {
records: StudioScriptFormUpdateInput[];
};

Return:

type StudioScriptFormBatchUpdateResult = {
items: StudioScriptFormRecord[];
total: number;
};

Parameters:

ParameterTypeRequiredDescription
recordsStudioScriptFormUpdateInput[]YesRecords to update. Minimum 1, maximum 100

batchUpdate runs in one transaction. If any record fails validation, permission checks, auto-generated field recalculation, post-update job enqueueing, or approval job enqueueing, the SDK call rejects and all updates in the batch are rolled back.

Update Parent And Child Records​

const result = await context.form.updateWithChildTables({
recordId: 'parent-record-id',
title: 'SO-1005',
data: {
status: 'confirmed',
},
submitApproval: true,
childTables: [
{
childFormApiName: 'order_items',
records: [
{
recordId: 'child-record-id-1',
data: {
quantity: 2,
},
},
{
recordId: 'child-record-id-2',
data: {
quantity: 1,
},
submitApproval: true,
},
],
},
],
});

Input:

type StudioScriptFormChildTableUpdateInput = {
childFormApiName: string;
records: StudioScriptFormUpdateInput[];
};

type StudioScriptFormUpdateWithChildTablesInput = StudioScriptFormUpdateInput & {
childTables: StudioScriptFormChildTableUpdateInput[];
};

Return:

type StudioScriptFormUpdateWithChildTablesResult = {
record: StudioScriptFormRecord;
childTables: Array<{
childFormApiName: string;
items: StudioScriptFormRecord[];
total: number;
}>;
};

updateWithChildTables updates one parent record and the specified child-table records in one transaction. Child rows are updated by recordId; the SDK does not create missing child rows in this API. If the parent record is missing, the method returns null. If any parent update, child-table lookup, child update, post-update job enqueueing, or approval job enqueueing fails, the SDK call rejects and the whole transaction is rolled back.

Batch Update Parent And Child Records​

const result = await context.form.batchUpdateWithChildTables({
records: [
{
recordId: 'parent-record-id-1',
data: {
status: 'confirmed',
},
childTables: [
{
childFormApiName: 'order_items',
records: [
{
recordId: 'child-record-id-1',
data: {
quantity: 3,
},
},
],
},
],
},
{
recordId: 'parent-record-id-2',
data: {
status: 'approved',
},
submitApproval: true,
childTables: [
{
childFormApiName: 'order_items',
records: [
{
recordId: 'child-record-id-2',
data: {
quantity: 5,
},
submitApproval: true,
},
],
},
],
},
],
});

Input:

type StudioScriptFormBatchUpdateWithChildTablesInput = {
records: Array<StudioScriptFormUpdateInput & {
childTables: StudioScriptFormChildTableUpdateInput[];
}>;
};

Return:

type StudioScriptFormBatchUpdateWithChildTablesResult = {
items: StudioScriptFormUpdateWithChildTablesResult[];
total: number;
};

batchUpdateWithChildTables updates multiple parent records and their child-table records in one transaction. Every parent and child update still follows the same permission checks, auto-generated field recalculation rules, post-update job enqueueing, and optional approval job enqueueing as the single-record APIs. If any parent record is missing or any update step fails, the SDK call rejects and the whole batch is rolled back.

Trash Or Delete A Record​

await context.form.trash({
recordId: record.id,
submitApproval: true,
});
await context.form.delete({
recordId: record.id,
});
const trashResult = await context.form.batchTrash({
recordIds: selectedRecordIds,
});

const deleteResult = await context.form.batchDelete({
recordIds: staleRecordIds,
});

Input:

type StudioScriptFormDeleteInput = {
recordId: string;
buttonKey?: string | null;
submitApproval?: boolean;
};

type StudioScriptFormRestoreInput = {
recordId: string;
buttonKey?: string | null;
title?: string;
data?: Record<string, unknown>;
dataStatus?: 'draft' | 'active' | 'archived';
};

type StudioScriptFormBatchDeleteInput = {
recordIds: string[];
buttonKey?: string | null;
submitApproval?: boolean;
};

type StudioScriptFormBatchTrashResult = {
requestedCount: number;
trashedCount: number;
trashedRecordIds: string[];
failed: Array<{
recordId: string;
code: string;
}>;
};

type StudioScriptFormBatchDeleteResult = {
requestedCount: number;
deletedCount: number;
deletedRecordIds: string[];
failed: Array<{
recordId: string;
code: string;
}>;
};

Parameters:

ParameterTypeRequiredDescription
recordIdstringYesTarget form record ID
recordIdsstring[]YesTarget form record IDs for batchTrash or batchDelete. Minimum 1, maximum 100
buttonKeystring | nullNoOptional button permission key. When provided, button execution permission is checked
submitApprovalbooleanNoFor trash and batchTrash, when true, tries to submit delete approval first and falls back to direct trash when no delete approval matches. For delete and batchDelete, true is not supported and returns FORM_DATA_DELETE_APPROVAL_NOT_SUPPORTED

Batch methods return partial results. Records that cannot be moved or deleted are listed in failed with the runtime error code, while successful records are listed in trashedRecordIds or deletedRecordIds.

Prefer trash for normal business flows. Use trash or batchTrash with submitApproval: true when the script explicitly needs delete approval. Use delete only for scripts that intentionally perform a hard delete.

Restore A Trashed Record​

Use restore for a soft-deleted record. It can update the title, field data, and restored data status in the same operation:

const restored = await context.form.restore({
recordId: trashedRecordId,
dataStatus: 'active',
data: {
restoreReason: 'Confirmed by operations',
},
});
ParameterTypeRequiredDescription
recordIdstringYesID of the record in the trash
buttonKeystring | nullNoOptional product button key whose execution permission should be enforced
titlestringNoNew title applied during restore
dataRecord<string, unknown>NoField values merged into the restored record
dataStatus'draft' | 'active' | 'archived'NoData status after restore

restore only applies to soft-deleted records. A permanently deleted record cannot be restored.

Saved Filter Client​

context.formSavedFilter manages personal saved filters owned by the current runtime actor. It does not create tenant-wide shared filters or modify another member's filters.

const saved = await context.formSavedFilter.save({
formApiName: 'orders',
name: 'High-value orders awaiting action',
condition: {
logic: 'and',
children: [
{ field: 'status', operator: 'eq', value: 'pending' },
{ field: 'amount', operator: 'gte', value: 10000 },
],
},
});

condition must be a root condition group and uses field API names. Omit id to create a filter. Pass an existing filter's real id to update it:

const filters = await context.formSavedFilter.list({
formApiName: 'orders',
});

const current = filters.items.find(
(item) => item.name === 'High-value orders awaiting action',
);
if (current) {
await context.formSavedFilter.save({
formApiName: 'orders',
id: current.id,
name: current.name,
condition: {
logic: 'and',
children: [{ field: 'status', operator: 'eq', value: 'pending' }],
},
});
}

Delete with filterId. The result is { id }, not { deleted }:

await context.formSavedFilter.delete({ filterId: saved.id });

Table View Client​

context.formTableView manages personal table views owned by the current runtime actor. It stores layout, grouping, and display preferences without modifying form design or another member's views.

const view = await context.formTableView.save({
formApiName: 'orders',
name: 'Grouped by customer',
layoutIds: [listLayoutId],
groupFieldIds: [customerFieldId],
scopes: ['list'],
bordered: true,
showRowNumber: true,
groups: [
{
id: 'customer-group',
name: 'Customer',
fieldIds: [customerFieldId],
level: 1,
parentId: null,
},
],
});
ParameterDescription
formApiNameTarget form API name, not an internal form ID
idExisting view ID when updating; omit when creating
layoutIdsLayout IDs used by the view
groupFieldIdsField IDs used for grouping, not field API names
scopeslist, detail, new, edit, or relation; defaults to list
borderedWhether table borders are shown
showRowNumberWhether row numbers are shown
groupsUp to three group levels, referencing field IDs through fieldIds

There is no columns input. Resolve real layout and field IDs from form configuration; do not substitute API names for IDs.

const views = await context.formTableView.list({ formApiName: 'orders' });
await context.formTableView.delete({ viewId: view.id });

Delete returns { id }. All three operations are scoped to views owned by the current runtime actor.

Form Schema Client​

Use studioScriptContext.formSchema when a script needs form or field metadata before reading or writing records.

interface StudioScriptFormSchemaClient {
get(input: StudioScriptFormSchemaGetInput): Promise<StudioScriptFormSchema>;
fields(input: StudioScriptFormSchemaFieldsInput): Promise<StudioScriptFormFieldList>;
field(input: StudioScriptFormSchemaFieldInput): Promise<StudioScriptFormField | null>;
}

Client methods:

MethodInputReturnDescription
getStudioScriptFormSchemaGetInputStudioScriptFormSchemaGets form metadata by form API name
fieldsStudioScriptFormSchemaFieldsInputStudioScriptFormFieldListLists fields for a form, with an optional keyword filter
fieldStudioScriptFormSchemaFieldInputStudioScriptFormField | nullGets one field by form API name and field API name

Get Form Metadata​

const form = await context.formSchema.get({
formApiName: 'orders',
});

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name. The runtime resolves and returns the accessible form metadata

Form Shape​

type StudioScriptFormSchema = {
id: string;
name: string;
apiName: string;
type: string;
description: string | null;
icon: string | null;
isDefault: boolean;
enabled: boolean;
};

List Fields​

const fields = await context.formSchema.fields({
formApiName: 'orders',
keyword: 'amount',
});

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name
keywordstring | nullNoOptional keyword used to filter fields by name or API name

Get One Field​

const amountField = await context.formSchema.field({
formApiName: 'orders',
fieldApiName: 'amount',
});

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name
fieldApiNamestringYesField API name to resolve within the target form

Field Shape​

type StudioScriptFormField = {
id: string;
formId: string;
groupId: string | null;
name: string;
apiName: string;
type: string;
dataType: string;
sortOrder: number;
required: boolean;
readonly: boolean;
disabled: boolean;
hidden: boolean;
isDefault: boolean;
isIdentifier: boolean;
defaultValue: unknown;
options: unknown;
validation: unknown;
display: unknown;
specialConfig: unknown;
config: unknown;
};

Field metadata is filtered by the current actor's form and field metadata permissions.

Built-in form, field, and option labels are returned as fallback English display text in the Studio SDK. Product pages still translate the stored i18n: keys with the current page locale.

File Client​

Use studioScriptContext.file when a script needs to resolve file assets from form file fields or create a new file asset.

interface StudioScriptFileClient {
get(input: StudioScriptFileGetInput): Promise<StudioScriptFileAsset | null>;
resolve(input: StudioScriptFileResolveInput): Promise<StudioScriptFileAsset[]>;
create(input: StudioScriptFileCreateInput): Promise<StudioScriptFileAsset>;
}

Client methods:

MethodInputReturnDescription
getStudioScriptFileGetInputStudioScriptFileAsset | nullGets one file asset by file ID
resolveStudioScriptFileResolveInputStudioScriptFileAsset[]Resolves multiple file assets by file IDs
createStudioScriptFileCreateInputStudioScriptFileAssetCreates one file asset from base64 content

Use file.previewUrl || file.url when a script needs a display URL for an existing file asset. previewUrl is available only for file types that the current preview viewer supports, such as common images, PDFs, text/code files, and common audio/video files. Files that are valid for upload and download can still have previewUrl: null.

Get One File​

const file = await context.file.get({
fileId: '6d4b0e70-8a36-4c11-a6f2-6ad1a7f6aa01',
});

Parameters:

ParameterTypeRequiredDescription
fileIdstringYesFile asset ID

Resolve Multiple Files​

const files = await context.file.resolve({
fileIds: ['file-id-1', 'file-id-2'],
});

Parameters:

ParameterTypeRequiredDescription
fileIdsstring[]YesFile asset IDs to resolve

Create A File​

const csv = 'orderNo,amount\\nSO-1001,1200\\n';
const file = await context.file.create({
name: 'orders.csv',
mimeType: 'text/csv',
size: csv.length,
contentBase64: btoa(csv),
});

Parameters:

ParameterTypeRequiredDescription
namestringYesFile display name
originalNamestringNoOriginal file name. Defaults to name
mimeTypestringYesMIME type, such as text/csv or application/pdf
sizenumberYesExpected file size in bytes
contentBase64stringYesFile content encoded as base64
storagePolicyKeystring | nullNoOptional storage policy key. Omit it to use the default policy

File APIs are scoped to the current tenant. They do not expose a hard-delete operation to Studio scripts.

Approval Client​

Use studioScriptContext.approval when a script needs to submit records into approval, inspect approval instances, or act on the current actor's approval tasks.

type StudioScriptApprovalRecordListInput = {
cursor?: string | null;
limit?: number;
status?: StudioScriptApprovalStatus | null;
definitionId?: string | null;
initiatorMemberId?: string | null;
currentApproverMemberId?: string | null;
startedAtFrom?: string | null;
startedAtTo?: string | null;
keyword?: string | null;
};

type StudioScriptApprovalRecordListItem = {
id: string;
definitionId: string;
definitionName: string;
recordId: string;
recordTitle: string;
formApiName: string;
formName: string;
initiatorType: string;
initiatorId: string;
initiatorName: string;
initiatorDisplayName: string | null;
currentApproverPrincipals: Array<{ principalType: string; principalId: string; displayName: string | null }>;
taskIds: string[];
currentNodeName: string;
currentApproverName: string;
taskStatus: string;
instanceStatus: string;
instanceResult: string | null;
startedAt: string;
completedAt: string | null;
};

type StudioScriptApprovalRecordListResult = {
items: StudioScriptApprovalRecordListItem[];
hasMore: boolean;
nextCursor: string | null;
limit: number;
};
interface StudioScriptApprovalClient {
getSubmissionConfig(input: { formApiName: string; action?: 'create' | 'update' | 'delete' }): Promise<{
enabled: boolean;
allowWithdraw: boolean;
definitionId: string | null;
definitionVersionId: string | null;
definitionName: string | null;
}>;
submitRecord(input: { recordId: string; action?: 'create' | 'update' | 'delete'; beforeRecordData?: Record<string, unknown> | null }): Promise<StudioScriptApprovalSubmitRecordResult>;
listRecords(input?: StudioScriptApprovalRecordListInput): Promise<StudioScriptApprovalRecordListResult>;
listRecordInstances(input: { recordId: string }): Promise<StudioScriptApprovalRecordInstanceSummary[]>;
getInstance(input: { instanceId: string }): Promise<StudioScriptApprovalInstance | null>;
listTasks(input?: { cursor?: string | null; limit?: number; status?: string | null; memberId?: string | null; timeRange?: { from: string; to: string } | null }): Promise<{ items: StudioScriptApprovalTaskListItem[]; hasMore: boolean; nextCursor: string | null; limit: number; timeRange: { from: string; to: string } }>;
getTask(input: { taskId: string }): Promise<StudioScriptApprovalTaskDetail | null>;
decideTask(input: { taskId: string; decision: 'approved' | 'rejected' | 'returned'; comment?: string | null; returnTarget?: StudioScriptApprovalReturnTarget | null }): Promise<StudioScriptApprovalTaskDecisionResult>;
withdraw(input: { instanceId: string; comment?: string | null }): Promise<{
instance: StudioScriptApprovalInstance;
record: {
recordId: string;
formId: string;
dataStatus: string;
approvalStatus: string;
approvalFlowInstanceId: string | null;
} | null;
}>;
}

Client methods:

MethodInputReturnDescription
getSubmissionConfig{ formApiName: string; action?: 'create' | 'update' | 'delete' }{ enabled; allowWithdraw; definitionId; definitionVersionId; definitionName }Gets approval submission configuration for a form action
submitRecord{ recordId: string; action?: 'create' | 'update' | 'delete'; beforeRecordData?: Record<string, unknown> | null }StudioScriptApprovalSubmitRecordResultSubmits a record into approval and returns a compact submission summary
listRecordsStudioScriptApprovalRecordListInputStudioScriptApprovalRecordListResultLists compact records visible through the approval runtime using cursor pagination
listRecordInstances{ recordId: string }StudioScriptApprovalRecordInstanceSummary[]Lists compact approval instance summaries related to one record
getInstance{ instanceId: string }StudioScriptApprovalInstance | nullGets one approval instance by instance ID
listTasks{ cursor?: string; limit?: number; status?: string; memberId?: string; timeRange?: { from: string; to: string } }{ items: StudioScriptApprovalTaskListItem[]; hasMore: boolean; nextCursor: string | null; limit: number; timeRange: { from: string; to: string } }Lists compact approval task summaries using cursor pagination
getTask{ taskId: string }StudioScriptApprovalTaskDetail | nullGets one compact approval task summary by task ID
decideTask{ taskId: string; decision: 'approved' | 'rejected' | 'returned'; comment?: string | null; returnTarget?: StudioScriptApprovalReturnTarget | null }StudioScriptApprovalTaskDecisionResultApproves, rejects, or returns one approval task
withdraw{ instanceId: string; comment?: string | null }{ instance: StudioScriptApprovalInstance; record: object | null }Withdraws a pending approval instance when the current actor is allowed

Get Submission Config​

const config = await context.approval.getSubmissionConfig({
formApiName: 'orders',
action: 'update',
});

Parameters:

ParameterTypeRequiredDescription
formApiNamestringYesForm API name. The runtime resolves the form ID internally
action'create' | 'update' | 'delete'NoTrigger action. Defaults to update

Submit Record​

const result = await context.approval.submitRecord({
recordId: 'record-id',
action: 'update',
});

Parameters:

ParameterTypeRequiredDescription
recordIdstringYesForm record ID
action'create' | 'update' | 'delete'NoTrigger action. Defaults to update
beforeRecordDataRecord<string, unknown> | nullNoPrevious record data snapshot for update/delete approval scenarios

Return shape:

type StudioScriptApprovalSubmitRecordResult = {
submitted: boolean;
config: {
enabled: boolean;
definitionName: string | null;
allowWithdraw: boolean;
};
record: {
recordId: string;
dataStatus: string;
approvalStatus: string;
approvalFlowInstanceId: string | null;
};
instance: {
instanceId: string;
status: string;
result: string | null;
startedAt: string;
completedAt: string | null;
} | null;
currentTask: {
taskId: string;
status: string;
nodeKey: string;
nodeName: string;
assigneeType: string;
assigneeId: string;
assigneeDisplayName: string | null;
} | null;
};

submitRecord returns a compact summary for scripts. It does not include full instance snapshots, subject records, node history, child table data, or form field data. Use listRecordInstances or getInstance when the script needs full approval runtime details.

List Records​

const records = await context.approval.listRecords({
limit: 20,
status: 'pending',
currentApproverMemberId: 'member-id',
});

Parameters:

ParameterTypeRequiredDescription
cursorstring | nullNoOpaque nextCursor returned by the previous call. Keep the remaining query conditions unchanged
limitnumberNoMaximum number of records to return. Defaults to 20; maximum is 100
statusStudioScriptApprovalStatus | nullNoFilters by task status
definitionIdstring | nullNoFilters by approval definition
initiatorMemberIdstring | nullNoFilters by initiating member
currentApproverMemberIdstring | nullNoFilters by current approver member
startedAtFrom / startedAtTostring | nullNoFilters by started time range
keywordstring | nullNoKeyword search

listRecords uses the same approval runtime visibility and permission rules as the approval record page.

List Record Instances​

const instances = await context.approval.listRecordInstances({
recordId: 'record-id',
});

Parameters:

ParameterTypeRequiredDescription
recordIdstringYesForm record ID

Return shape:

type StudioScriptApprovalRecordInstanceSummary = {
instanceId: string;
status: string;
result: string | null;
definitionName: string;
allowWithdraw: boolean;
subjectId: string;
startedAt: string;
completedAt: string | null;
currentTasks: Array<{
taskId: string;
status: string;
nodeKey: string;
nodeName: string;
assigneeType: string;
assigneeId: string;
assigneeDisplayName: string | null;
}>;
};

listRecordInstances returns compact instance summaries and current pending tasks for scripts. It does not include full instance snapshots, subject records, node history, child table data, or form field data. Use getInstance when the script needs full approval runtime details.

Get Instance​

const instance = await context.approval.getInstance({
instanceId: 'approval-instance-id',
});

Parameters:

ParameterTypeRequiredDescription
instanceIdstringYesApproval instance ID

getInstance returns full approval instance details, including instance snapshots, node history, task list, and subject records. Prefer listRecordInstances when the script only needs compact summaries.

List Tasks​

const tasks = await context.approval.listTasks({
status: 'pending',
});

Parameters:

ParameterTypeRequiredDescription
cursorstring | nullNoCursor returned by the previous task-list call
limitnumberNoMaximum number of tasks returned per call. Defaults to 20; maximum is 100
statusstring | nullNoOptional task status filter
memberIdstring | nullNoMember ID to query. When provided, the SDK lists tasks visible/actionable through that member's principals
timeRange{ from: string; to: string } | nullNoOptional inclusive time range for task search. When omitted, the approval runtime applies its default range

Return shape:

type StudioScriptApprovalTaskListItem = {
taskId: string;
instanceId: string;
status: string;
nodeKey: string;
nodeName: string;
assigneeType: string;
assigneeId: string;
assigneeDisplayName: string | null;
subjectId: string;
instanceStatus: string;
createdAt: string;
};

listTasks returns compact task summaries for scripts. It does not include tenantId, approval policy fields, comments, full subjectSnapshot, child table data, or form field data. Use getTask when the script needs one task's decision result.

Get Task​

const task = await context.approval.getTask({
taskId: 'task-id',
});

Return shape:

type StudioScriptApprovalTaskDetail = StudioScriptApprovalTaskListItem & {
decidedAt: string | null;
decision: string | null;
comment: string | null;
};

getTask returns a compact task summary for scripts. It does not include full subjectSnapshot, child table data, or form field data. Use subjectId with the form SDK when the script needs the related business record.

Decide Task​

await context.approval.decideTask({
taskId: 'task-id',
decision: 'approved',
comment: 'Looks good.',
});

Parameters:

ParameterTypeRequiredDescription
taskIdstringYesApproval task ID
decision'approved' | 'rejected' | 'returned'YesTask decision
commentstring | nullNoDecision comment. Some approval nodes may require it
returnTarget{ type: 'initiator' } | { type: 'node'; nodeKey: string } | nullNoRequired when returning to a specific node or initiator

Return shape:

type StudioScriptApprovalTaskDecisionResult = {
task: {
taskId: string;
status: string;
decision: string | null;
comment: string | null;
decidedAt: string | null;
} | null;
instance: {
instanceId: string;
status: string;
result: string | null;
completedAt: string | null;
currentTasks: StudioScriptApprovalTaskSummary[];
};
};

decideTask uses the same approval runtime permissions and node decision rules as the product UI. Nodes may reject unsupported decisions, missing comments, or missing return targets.

Withdraw Instance​

const result = await context.approval.withdraw({
instanceId: 'approval-instance-id',
comment: 'Withdraw by script.',
});

Parameters:

ParameterTypeRequiredDescription
instanceIdstringYesPending approval instance ID to withdraw
commentstring | nullNoWithdraw comment

withdraw only succeeds when the current actor is allowed to withdraw the instance and the instance is still pending.

Organization Client​

Use studioScriptContext.organization when a script needs read-only organization profile form data from the organization table.

interface StudioScriptOrganizationClient {
get(input: StudioScriptOrganizationGetInput): Promise<StudioScriptOrganizationProfileData | null>;
list(input?: StudioScriptOrganizationListInput): Promise<StudioScriptOrganizationList>;
tree(input?: Record<string, never>): Promise<StudioScriptOrganizationTree>;
}

type StudioScriptOrganizationProfileData = {
data: Record<string, unknown>;
};

type StudioScriptOrganizationList = {
items: StudioScriptOrganizationProfileData[];
};

type StudioScriptOrganizationTree = {
items: StudioScriptOrganizationTreeNode[];
};

interface StudioScriptOrganizationTreeNode extends StudioScriptOrganizationProfileData {
children: StudioScriptOrganizationTreeNode[];
}

Client methods:

MethodInputReturnDescription
getStudioScriptOrganizationGetInputStudioScriptOrganizationProfileData | nullGets one organization's profile form data by organization ID
listStudioScriptOrganizationListInputStudioScriptOrganizationListLists organization profile form data, with an optional keyword filter
treeRecord<string, never>StudioScriptOrganizationTreeGets the organization profile form data tree. No parameters are required

List Organizations​

const organizations = await context.organization.list({
keyword: 'Sales',
condition: {
logic: 'and',
children: [{ field: 'so_status', operator: 'eq', value: 'active' }],
},
});

Parameters:

ParameterTypeRequiredDescription
keywordstring | nullNoFilters by organization name or code
conditionStudioScriptFormConditionGroup | nullNoFilters by organization profile field API names using the form-record condition operators

Get Organization Tree​

const tree = await context.organization.tree();

tree does not require parameters.

Get One Organization​

const organization = await context.organization.get({
organizationId: 'organization-id',
});

Parameters:

ParameterTypeRequiredDescription
organizationIdstringYesOrganization ID

The Organization Client is read-only in Studio scripts. It does not expose organization create, update, move, deactivate, or delete operations.

Department Client​

Use studioScriptContext.department when a script needs read-only department profile form data.

interface StudioScriptDepartmentClient {
get(input: StudioScriptDepartmentGetInput): Promise<StudioScriptDepartmentProfileData | null>;
list(input?: StudioScriptDepartmentListInput): Promise<StudioScriptDepartmentList>;
tree(input?: { organizationId?: string | null }): Promise<StudioScriptDepartmentTree>;
}

type StudioScriptDepartmentProfileData = {
data: Record<string, unknown>;
};

type StudioScriptDepartmentList = {
items: StudioScriptDepartmentProfileData[];
};

type StudioScriptDepartmentTree = {
items: StudioScriptDepartmentTreeNode[];
};

interface StudioScriptDepartmentTreeNode extends StudioScriptDepartmentProfileData {
children: StudioScriptDepartmentTreeNode[];
}

Client methods:

MethodInputReturnDescription
getStudioScriptDepartmentGetInputStudioScriptDepartmentProfileData | nullGets one department's profile form data by department ID
listStudioScriptDepartmentListInputStudioScriptDepartmentListLists department profile form data, with optional keyword and organization filters
tree{ organizationId?: string | null }StudioScriptDepartmentTreeGets the department profile form data tree, optionally filtered by organization ID

List Departments​

const departments = await context.department.list({
keyword: 'Sales',
condition: {
logic: 'and',
children: [{ field: 'so_status', operator: 'eq', value: 'active' }],
},
});

Parameters:

ParameterTypeRequiredDescription
keywordstring | nullNoFilters by department name or code
organizationIdstring | nullNoFilters departments by organization ID
conditionStudioScriptFormConditionGroup | nullNoFilters by department profile field API names using the form-record condition operators

Get Department Tree​

const tree = await context.department.tree();

Parameters:

ParameterTypeRequiredDescription
organizationIdstring | nullNoFilters the tree by organization ID

Get One Department​

const department = await context.department.get({
departmentId: 'department-id',
});

Parameters:

ParameterTypeRequiredDescription
departmentIdstringYesDepartment ID

The Department Client is read-only in Studio scripts. It does not expose department create, update, move, deactivate, or delete operations.

Member Client​

Use studioScriptContext.member when a script needs read-only member data.

interface StudioScriptMemberClient {
current(input?: Record<string, never>): Promise<StudioScriptCurrentMember | null>;
get(input: StudioScriptMemberGetInput): Promise<StudioScriptMemberProfileData | null>;
list(input?: StudioScriptMemberListInput): Promise<StudioScriptMemberList>;
}

Client methods:

MethodInputReturnDescription
currentRecord<string, never>StudioScriptCurrentMember | nullGets the profile form data for the member that triggered the current script. No parameters are required
getStudioScriptMemberGetInputStudioScriptMemberProfileData | nullGets one member's profile form data by member ID
listStudioScriptMemberListInputStudioScriptMemberListLists member profile form data, with optional keyword, organization, department, and pagination filters

Get Current Member​

const member = await context.member.current();

current does not require parameters. It returns null when the script is triggered without a logged-in member context.

Return shape:

type StudioScriptCurrentMember = {
data: Record<string, unknown>;
};

data is the current member's profile form data.

List Members​

const members = await context.member.list({
departmentId: 'department-id',
includeDescendants: true,
limit: 20,
condition: {
logic: 'and',
children: [{ field: 'so_avatar', operator: 'not_empty' }],
},
});

items contains only member profile form data:

type StudioScriptMemberList = {
items: Array<{ data: Record<string, unknown> }>;
hasMore: boolean;
nextCursor: string | null;
limit: number;
summary: {
active: number;
inactive: number;
};
};

Parameters:

ParameterTypeRequiredDescription
keywordstring | nullNoFilters by member name, email, phone, or employee code
organizationIdstring | nullNoFilters members by organization ID
departmentIdstring | nullNoFilters members by department ID
includeDescendantsbooleanNoIncludes members from child departments when departmentId is provided
cursorstring | nullNoOpaque nextCursor returned by the previous call. Keep the remaining query conditions unchanged
limitnumberNoMaximum number of members to return. Defaults to 20; maximum is 100
conditionStudioScriptFormConditionGroup | nullNoFilters by member profile field API names using the form-record condition operators

Get One Member​

const member = await context.member.get({
memberId: 'member-id',
});

get returns the same { data } shape as current.

Parameters:

ParameterTypeRequiredDescription
memberIdstringYesMember ID

The Member Client is read-only in Studio scripts. It does not expose member create, update, deactivate, or delete operations.

IAM Client​

Use studioScriptContext.iam when a script needs to check the current runtime actor's effective roles. This is a read-only runtime SDK, not a role management API.

interface StudioScriptIamClient {
currentRoleSummary(input?: Record<string, never>): Promise<{
roles: StudioScriptIamRole[];
roleCodes: string[];
}>;
memberRoleSummary(input: { memberId: string }): Promise<{
memberId: string;
roles: StudioScriptIamRole[];
roleCodes: string[];
}>;
memberRoleSummaries(input: { memberIds: string[] }): Promise<{
items: Array<{
memberId: string;
roles: StudioScriptIamRole[];
roleCodes: string[];
}>;
}>;
}

Client methods:

MethodInputReturnDescription
currentRoleSummaryRecord<string, never>{ roles: StudioScriptIamRole[]; roleCodes: string[] }Gets the current runtime actor's role summary. No parameters are required
memberRoleSummary{ memberId: string }{ memberId: string; roles: StudioScriptIamRole[]; roleCodes: string[] }Gets one specified member's role summary
memberRoleSummaries{ memberIds: string[] }{ items: Array<MemberRoleSummary> }Gets role summaries for multiple specified members

Get Current Role Summary​

const summary = await context.iam.currentRoleSummary();

currentRoleSummary does not require parameters. It returns active roles matched through the current actor's member, department, and organization principals, plus de-duplicated roleCodes for simple script branching.

Get Member Role Summary​

const summary = await context.iam.memberRoleSummary({
memberId: 'member-id',
});

Parameters:

ParameterTypeRequiredDescription
memberIdstringYesMember ID in the current tenant

memberRoleSummary returns active roles matched through the target member's member, department, and organization principals, plus de-duplicated roleCodes.

Get Member Role Summaries​

const summaries = await context.iam.memberRoleSummaries({
memberIds: ['member-id-1', 'member-id-2'],
});

Parameters:

ParameterTypeRequiredDescription
memberIdsstring[]YesMember IDs in the current tenant. Maximum 100 per call

memberRoleSummaries returns one summary per requested member ID, preserving request order.

Role Shape​

type StudioScriptIamRole = {
id: string;
code: string;
name: string;
description: string | null;
builtIn: boolean;
systemProtected: boolean;
status: 'active';
bindings: Array<{
id: string;
principalType: 'MEMBER' | 'DEPARTMENT' | 'ORGANIZATION';
principalId: string;
precedence: number;
startAt: string | null;
endAt: string | null;
}>;
};

Role fields:

FieldTypeDescription
idstringRole ID. Use it when you need to correlate with role bindings or other role records
codestringStable role code for script branching, such as space_admin or a custom role code
namestringDisplay name of the role
descriptionstring | nullOptional role description
builtInbooleanWhether the role is a system built-in role
systemProtectedbooleanWhether the role is protected from normal user-side modification
status'active'Role status. The SDK only returns active roles
bindingsStudioScriptIamRoleBinding[]Active bindings that matched the queried member/current actor

Binding fields:

FieldTypeDescription
idstringRole binding ID
principalType'MEMBER' | 'DEPARTMENT' | 'ORGANIZATION'Principal type that grants the role
principalIdstringID of the member, department, or organization principal
precedencenumberMatching priority. Lower numbers are more specific; member bindings are 0, department bindings are 1, organization bindings are 2
startAtstring | nullBinding start time. null means it is effective immediately
endAtstring | nullBinding end time. null means it does not expire

Only enabled bindings in their valid time window are returned. Disabled roles and inactive bindings are filtered out.

Custom login page​

Install​

npm i @soln/custom-login-sdk

Use the complete demo​

Download the demo from GitHub or GitCode. SDK calls are already integrated; English comments SOLN INTEGRATION 1–9 mark their locations.

Edit examples/config.js:

export const config = {
issuer: 'https://console.example.com', // Your trusted Soln control-plane origin.
workspaceLoginUrl: 'https://work.example.com/auth/start', // Your workspace login entry.
brandName: 'Your brand',
faviconUrl: './assets/favicon-192x192.png',
locale: 'en',
};

Replace the logo in examples/index.html, text in examples/messages.js, and styles in examples/styles.css as needed. Deploy examples/ and dist/ together to an HTTPS site, preserving their relative paths.

In System Management → Organization Profile → Edit, enter the deployed /examples/ URL in Custom Login Page URL and save. Leave it empty to restore the default page. Start login from the workspace, or open the demo directly with workspaceLoginUrl configured. No existing login session is needed.

Connect your own page​

Create a client when the page opens, then bind its methods to your form. Set issuer to your trusted control-plane origin.

import {
createSolnLoginClient,
type LoginResult,
} from '@soln/custom-login-sdk';

const login = createSolnLoginClient({
issuer: 'https://console.example.com',
locale: 'en',
});

const context = await login.getContext();
// If context.passwordSetup is present, show the new-password form.

function finish(result: LoginResult) {
if (result.nextStep === 'set_password') {
// Show the new-password form for result.email.
return;
}
login.complete(result);
}

// Bind to the password-login form.
async function submitPassword(email: string, password: string) {
finish(await login.loginWithPassword({ email, password }));
}

// Bind to the send-code button.
async function sendCode(email: string) {
await login.sendEmailCode({ email });
}

// Bind to the email-code form. The code has six digits.
async function submitCode(email: string, code: string) {
finish(await login.verifyEmailCode({ email, code }));
}

// Bind to the new-password form. Use at least eight characters.
async function submitNewPassword(newPassword: string) {
finish(await login.setupPassword({ newPassword }));
}

Show errors through your page's notification component and restore button loading state in finally. Human verification opens automatically through the SDK when needed. The complete demo includes these behaviors.

Always call complete() for a complete result to finish login and return to the workspace. If the login request expires, start again from the workspace /auth/start entry.

Permissions And Errors​

Studio SDK calls run as the current script actor and reuse the same tenant-side permission checks as the product runtime. They do not bypass form visibility, row access, field access, button permissions, approval task permissions, or tenant file boundaries.

When an SDK call throws and the script does not catch it, the script execution fails with STUDIO_ASSET_EXECUTION_FAILED. The execution result's errorMessage is the underlying runtime code, such as IAM_PERMISSION_DENIED, FORMS_SCHEMA_NOT_FOUND, or FORM_DATA_RECORD_ID_REQUIRED.

Permission Rules​

ClientPermission behavior
formResolves forms by formApiName, checks form visibility, applies row and field access for reads, and uses the normal create/update/delete runtime flows for writes. buttonKey triggers the same button execution permission checks as product buttons
formSchemaReturns only form and field metadata visible to the current actor. Disabled or inaccessible forms are rejected
fileResolves and creates file assets only inside the current tenant. File IDs from another tenant are not accessible
approvalUses the normal approval runtime visibility and action rules. Task decisions and withdrawals must be actionable by the current actor or the member context supported by that method
organization / department / memberRead-only. Returns profile form data visible in the current tenant; it does not expose create, update, deactivate, move, or delete operations
iamRead-only. Returns role summaries for current or specified members; it does not expose role management or binding mutation APIs

Error Handling​

Use try/catch when the script should convert SDK failures into a controlled return value or a user-visible runtime effect.

export default async function main(_payload: StudioScriptRuntimeContextPayload, context: StudioScriptContext) {
try {
const record = await context.form.find({
formApiName: 'orders',
condition: {
logic: 'and',
children: [
{
field: 'recordId',
operator: 'eq',
value: 'record-id',
},
],
},
});

return {
ok: true,
record,
};
} catch (error) {
const code = error instanceof Error ? error.message : String(error);
return {
ok: false,
message: code,
notification: {
type: 'error',
title: 'Operation failed',
description: code,
},
};
}
}

Batch methods have two different failure styles:

  • batchTrash and batchDelete return partial results. Per-record failures are listed in failed with a code
  • Transactional batch create/update methods reject the SDK call when any record fails, and the transaction is rolled back

Common Error Codes​

General Studio SDK errors:

CodeMeaning
STUDIO_ASSET_EXECUTION_FAILEDThe script threw an uncaught error. The underlying SDK error is returned as errorMessage
STUDIO_FORM_API_INPUT_INVALIDThe form SDK input must be an object
STUDIO_FORM_SCHEMA_API_INPUT_INVALIDThe form schema SDK input must be an object
STUDIO_FORM_SAVED_FILTER_API_INPUT_INVALIDSaved filter SDK input must be an object
STUDIO_FORM_TABLE_VIEW_API_INPUT_INVALIDTable view SDK input must be an object
STUDIO_FILE_API_INPUT_INVALIDThe file SDK input must be an object
STUDIO_APPROVAL_API_INPUT_INVALIDThe approval SDK input must be an object
STUDIO_ORG_API_INPUT_INVALIDThe organization, department, or member SDK input must be an object
STUDIO_FORM_API_OPERATION_INVALIDUnknown form SDK operation
STUDIO_FORM_SCHEMA_API_OPERATION_INVALIDUnknown form schema SDK operation
STUDIO_FORM_SAVED_FILTER_API_OPERATION_INVALIDUnknown saved filter SDK operation
STUDIO_FORM_TABLE_VIEW_API_OPERATION_INVALIDUnknown table view SDK operation
STUDIO_FILE_API_OPERATION_INVALIDUnknown file SDK operation
STUDIO_APPROVAL_API_OPERATION_INVALIDUnknown approval SDK operation
STUDIO_ORG_API_OPERATION_INVALIDUnknown organization, department, or member SDK operation
STUDIO_IAM_API_OPERATION_INVALIDUnknown IAM SDK operation

Form and form schema errors:

CodeMeaning
FORMS_SCHEMA_API_NAME_REQUIREDformApiName is missing
FORMS_SCHEMA_NOT_FOUNDThe form API name cannot be resolved or is not accessible
FORMS_SCHEMA_DISABLEDThe target form is disabled
FORMS_FIELD_API_NAME_REQUIREDfieldApiName is missing
FORM_DATA_RECORD_ID_REQUIREDrecordId is missing
FORM_DATA_NOT_FOUNDThe target record cannot be found or is not accessible
FORM_DATA_APPROVAL_PENDING_READ_ONLYThe record is in a pending approval state and cannot be modified directly
FORM_DATA_APPROVAL_FLOW_REQUIREDThe operation requires an approval flow instance
FORM_DATA_APPROVAL_TRANSITION_INVALIDThe requested approval status transition is not allowed
FORM_DATA_DELETE_APPROVAL_NOT_SUPPORTEDHard delete does not support approval submission; use trash or batchTrash with submitApproval: true
FORM_DATA_FOREIGN_KEY_RESTRICTEDA referenced record or relation is restricted by data permissions or relation rules
IAM_PERMISSION_DENIEDThe current actor does not have the required permission
STUDIO_FORM_API_DATA_INVALIDdata is not an object
STUDIO_FORM_API_RECORDS_REQUIREDBatch create or update requires at least one record
STUDIO_FORM_API_RECORDS_LIMIT_EXCEEDEDBatch create or update exceeded the maximum record count
STUDIO_FORM_API_RECORD_IDS_REQUIREDBatch delete requires at least one record ID
STUDIO_FORM_API_RECORD_IDS_LIMIT_EXCEEDEDBatch delete exceeded the maximum record ID count
STUDIO_FORM_API_CHILD_TABLES_REQUIREDA create/update-with-child-tables call requires child table input
STUDIO_FORM_API_CHILD_TABLE_NOT_FOUNDThe child form API name is not a child table of the parent form
STUDIO_FORM_API_CHILD_RECORDS_REQUIREDA child table operation requires at least one child record
STUDIO_FORM_API_CHILD_RECORDS_LIMIT_EXCEEDEDA child table operation exceeded the maximum child record count
FORM_DATA_BATCH_UPDATE_RECORD_NOT_FOUNDA batch update target record was not found or not accessible

File errors:

CodeMeaning
FILES_ID_REQUIREDfileId is missing
FILES_IDS_REQUIREDfileIds is missing or empty
FILES_NAME_REQUIREDFile display name is missing
FILES_ORIGINAL_NAME_REQUIREDOriginal file name is missing
FILES_MIME_TYPE_REQUIREDMIME type is missing
FILES_CONTENT_REQUIREDBase64 file content is missing
FILES_NOT_FOUNDThe file asset cannot be found or is not accessible

Approval errors:

CodeMeaning
APPROVAL_TRIGGER_ACTION_INVALIDApproval action must be create, update, or delete
APPROVAL_TASK_ID_REQUIREDtaskId is missing
APPROVAL_INSTANCE_ID_REQUIREDinstanceId is missing
APPROVAL_TASK_DECISION_INVALIDTask decision must be approved, rejected, or returned
APPROVAL_TASK_NOT_FOUNDThe task cannot be found or is not visible to the actor
APPROVAL_TASK_NOT_ACTIONABLEThe task cannot be acted on by the current actor or in its current state
APPROVAL_TASK_ALREADY_DECIDEDThe task has already been decided
APPROVAL_TASK_COMMENT_REQUIREDThe current approval policy requires a comment
APPROVAL_INSTANCE_NOT_FOUNDThe approval instance cannot be found or is not visible
APPROVAL_INSTANCE_ALREADY_PENDINGThe record already has a pending approval instance
APPROVAL_INSTANCE_NOT_ACTIONABLEThe approval instance cannot be acted on in its current state
APPROVAL_INSTANCE_WITHDRAW_DISABLEDWithdrawal is disabled for this approval definition
APPROVAL_INSTANCE_WITHDRAW_FORBIDDENThe current actor cannot withdraw this approval instance
APPROVAL_RETURN_TARGET_INVALIDThe return target is invalid
APPROVAL_RETURN_TARGET_NODE_REQUIREDA node return target requires nodeKey

Organization, department, member, and IAM errors:

CodeMeaning
ORGANIZATIONS_ID_REQUIREDorganizationId is missing
DEPARTMENTS_ID_REQUIREDdepartmentId is missing
MEMBERS_ID_REQUIREDmemberId is missing
STUDIO_IAM_MEMBER_ID_REQUIREDmemberId is missing for a role summary lookup
STUDIO_IAM_MEMBER_IDS_REQUIREDmemberIds is missing or empty
STUDIO_IAM_MEMBER_IDS_LIMIT_EXCEEDEDmemberIds exceeds the maximum count of 100