Skip to main content

Soln Studio

Soln Studio is used to build tenant-side scripts and custom pages. Scripts run on the server with StudioScriptContext. Page assets run in the browser and can declare server actions for work that needs member data, form APIs, files, approvals, or external HTTP calls.

Use the Studio page to define variables close to the runtime that owns the data:

  • Script assets define variables from payload and context
  • Page assets define browser state in src/App.tsx
  • Page server actions define server-side variables from action payload and context
  • Form values are read and written through field API names in record.data and write data

Asset Types​

TypeRuntimeEntryUse case
Scriptserver_jsindex.tsServer-side automation, form writes, approval service calls, and integration logic
Pageclient_pagesrc/App.tsxCustom user pages, dashboards, buttons, and pages that call server actions

New page assets use the standard project layout:

soln.extension.json
src
├── App.tsx
└── server
└── main.ts

Script Asset Starter​

Script assets start with a default index.ts file:

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 business values are available only under payload.input; source, form, record, draft, selection, child-row, previous-result, and runtime metadata keep their canonical top-level fields.

Defining Variables​

Define input types first when the script receives a known payload shape:

type CreateOrderInput = {
orderNo?: unknown;
amount?: unknown;
customerId?: unknown;
};

export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const input = payload.input as CreateOrderInput;
const orderNo = String(input.orderNo || '').trim();
const amount = Number(input.amount);
const customerId = String(input.customerId || '').trim();

if (!orderNo || !Number.isFinite(amount)) {
throw new Error('ORDER_INPUT_INVALID');
}

return context.form.create({
formApiName: 'orders',
title: orderNo,
data: {
orderNo,
amount,
customer: customerId || null,
},
});
}

When variables come from an existing record, use the field API name from record.data:

const record = await context.form.find({
formApiName: 'orders',
condition: {
logic: 'and',
children: [
{ field: 'recordId', operator: 'eq', value: 'record-id' },
],
},
});
const data = record?.data || {};
const orderNo = String(data.orderNo || '').trim();
const amount = Number(data.amount || 0);

Use context.formSchema.field or context.formSchema.fields when a script needs to inspect a field's type or dataType before converting values.

Built-in Methods​

Server-side scripts and server actions receive StudioScriptContext. The runtime already supports these built-in clients:

ClientCommon methodsUse
context.runtimeexecutionId, traceId, assetIdRead execution metadata
context.loggerinfo, warn, error, debugWrite Studio run logs
context.httprequest, requestJsonCall external HTTP APIs
context.formlist, find, create, update, trashRead and write form records
context.formSchemaget, fields, fieldRead form and field metadata
context.fileget, resolve, createResolve or create file assets
context.approvalsubmitRecord, listTasks, decideTaskWork with approval runtime data
context.organizationget, list, treeRead organization profile data
context.departmentget, list, treeRead department profile data
context.membercurrent, get, listRead member profile data
context.iamcurrentRoleSummary, memberRoleSummaryRead runtime role summaries

For full method signatures, see Soln SDK.

Page Asset Manifest​

Every standard page asset includes soln.extension.json.

{
"entryFilePath": "src/App.tsx",
"serverActions": [
{
"name": "main",
"entryFilePath": "src/server/main.ts"
}
]
}

entryFilePath points to the browser page entry. serverActions declares server-side TypeScript files inside the same page asset.

Only declared action files are executed on the server. Other .ts files remain normal page helper files.

Page Entry​

src/App.tsx is the browser page:

export default function App() {
return (
<main
style={{
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
padding: 24,
boxSizing: 'border-box',
fontFamily: 'Arial, sans-serif',
}}
>
Soln Studio Page
</main>
);
}

Local Imports​

Studio assets are compiled as controlled asset-local modules. Use relative imports for files inside the same asset:

import { formatMoney } from './shared/money';
import { OrderCard } from './client/components/OrderCard';

Page client files can import client and shared files, but must not import server action files directly. Call server actions through @soln/studio instead. Server actions can import server and shared helpers with relative paths.

Page client files can also import the built-in Ant Design 6 package from the project runtime:

import { Button, Card, Table } from 'antd';

Use the root antd import for Ant Design components. Other npm packages are still not available unless the platform adds them as built-ins.

When building Studio page UI, prefer Ant Design 6 components for forms, tables, cards, buttons, modals, drawers, tabs, alerts, empty/loading states, and common layout controls.

Built-in Dependencies and Compilation Failures​

The project currently locks Ant Design to 6.5.2. The ^6.5.2 dependency range is not a claim that this is the latest npm release; the Runtime release lockfile determines the installed version.

antd is available only to page client code, not server scripts or server actions. Import components from the root package; subpaths such as antd/es/*, antd/lib/*, and antd/dist/reset.css are not supported.

If compilation reports Cannot find module 'antd' with compiler-core.js in the require stack for a root import, the deployed Runtime cannot resolve its built-in dependency. The Runtime image must install antd as a production dependency and be rebuilt and upgraded. Updating only the frontend does not fix it. A successful compile in a full local repository does not verify the deployed container.

This is a blocking compilation error. Preserve the requested component-based page and report the Runtime repair needed; do not silently replace it with native elements and describe the dependency problem as fixed. An alternative UI can be used when the user requests or approves it. Runtime image builds check compilation of a page using Card, Table, and Tag; existing installations still require the corrected Runtime image.

Organize Pages and Scripts by Responsibility​

Prefer multiple cohesive files instead of putting an entire feature into one entry. Read the existing asset tree before editing and reuse appropriate modules.

  • Pages: keep src/App.tsx focused on composition. Put substantial UI sections in src/client/components/, data/state hooks in src/client/hooks/, server actions and business services in src/server/, and browser-safe types or pure helpers in src/shared/.
  • Scripts: keep the configured entry focused on orchestration. Separate validation, business rules, external API adapters, and reusable helpers into local modules when those responsibilities grow.
  • Use descriptive English file names and relative imports. Keep each file focused; split growing files before roughly 500–600 lines. This is a maintenance guideline, not a compiler limit. Small cohesive features do not need artificial fragmentation.
  • Client code must not import server modules. Shared modules must be safe in every runtime that imports them; call declared server actions through callAction.
  • Apply related file and import changes together, compile the complete asset with its active binding, inspect all diagnostics, then save and publish separately. A successful file write is not successful compilation or publication.

Server Actions​

Server actions are declared in soln.extension.json and run with the same StudioScriptContext as script assets.

export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const orderNo = String(payload.input.orderNo || '').trim();
return {
ok: true,
orderNo,
executionId: context.runtime.executionId,
};
}

Call an action from the page:

import { callAction } from '@soln/studio';

async function runAction() {
const result = await callAction<
{ orderNo: string },
{ ok: boolean; orderNo: string }
>('main', {
orderNo: 'SO-1001',
});

console.log(result);
}

The page never receives the user token directly. It sends a message to the host page, and the host calls the server action with the current session.

Read the page runtime context or theme from the same built-in SDK:

import { getRuntimeContext, getTheme } from '@soln/studio';

const context = getRuntimeContext();
const theme = getTheme();
const isDark = theme?.resolvedScheme === 'dark';

When a Studio page is opened by an interaction button on a form view or edit page, read the host record from the runtime context:

import { getRuntimeContext } from '@soln/studio';

const context = getRuntimeContext();
const recordId = context?.record?.id;
const draftData = context?.draft?.data || {};
const formApiName = context?.form?.apiName;
const selectedRecords = context?.selection?.records || [];

For pages opened by form buttons, current form values are in context.draft.data. context.record is null on create pages, and its data is not preloaded on view or edit pages. A server action that needs persisted values must call context.form.get({ recordId: payload.record.id }). Use context.selection.records or context.selection.childRows for selections and context.child.row for the current child row. Do not read the current record from URL query parameters unless the page is explicitly opened by URL navigation.

Preview And Run​

Use the debug panel to compile, preview, or run the current asset.

  • For script assets, Run executes index.ts
  • For page assets, Run previews the page when a normal page file is active
  • If the active file is declared in serverActions, Run executes that action directly and shows logs, errors, and returnValue

For example, opening src/server/main.ts and clicking Run executes only the main action. Opening src/App.tsx and clicking Run previews the page.

External HTTP And Member Data​

Server actions can read the current member and call external HTTP APIs:

export default async function createLoginUrl(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const member = await context.member.current();
const data = (member?.data || {}) as Record<string, unknown>;
const phone = String(data.phone || '').trim();

const response = await context.http.request({
url: 'https://example.com/session',
method: 'POST',
json: { phone },
timeoutMs: 10000,
expect: 'json',
});

return {
ok: response.ok,
data: response.bodyJson,
};
}

Keep temporary logs while checking real member fields, then remove them after the field mapping is confirmed.