Skip to main content

Create and link surfaces

Import Surface from @culvii/kit, create it in a .culvii.ts file, and assign it through a step's ui property.

Create an output surface

import { Step, Surface, Workflow } from '@culvii/kit';

const responseSummary = new Surface({
id: 'http-response-summary',
name: 'HTTP response summary',
description: 'Status and body returned by the HTTP request.',
slot: 'output',
definition: {
schema: 'kit.ui/v1',
root: 'layout',
components: [
{
id: 'layout',
type: 'Stack',
children: ['status', 'body'],
},
{
id: 'status',
type: 'StatusPill',
props: {
label: {
path: '$output/Get Second Resource/main/0/0/statusMessage',
default: 'Unknown',
},
value: {
path: '$output/Get Second Resource/main/0/0/statusCode',
},
},
},
{
id: 'body',
type: 'JsonInspector',
props: {
value: {
path: '$output/Get Second Resource/main/0/0/body',
},
},
},
],
},
});

definition.root is the ID of the top-level component. Components reference their children by ID, so they can be declared in any order.

Attach the local surface directly

Assign the Surface instance to the matching slot:

const getSecondResource = new Step({
id: 'get-second-resource',
name: 'Get Second Resource',
type: 'core.http',
params: {
method: 'GET',
url: 'https://api.example.com/resource',
},
ui: {
output: responseSummary,
},
});

The CLI discovers both resources when it evaluates the file. Surfaces are synced or deployed before workflows, so the workflow reference can be validated.

Reuse a surface

A single tenant-scoped surface can be assigned to multiple steps and workflows. Reference it by its id without redefining it:

const anotherRequest = new Step({
id: 'another-request',
name: 'Another Request',
type: 'core.http',
params: {
method: 'GET',
url: 'https://api.example.com/another-resource',
},
ui: {
output: {
kind: 'custom',
surfaceKey: 'http-response-summary',
fallbackSurfaceId: 'default.json',
},
},
});

For a highly reusable surface, bind a JsonInspector to $output or bind to a shared upstream step through $input. A path that hardcodes Get Second Resource only resolves when that step is present in the surface's data model.

Pin a published version

Omit version to follow the active session draft in development and the current published version after deployment:

ui: {
output: {
kind: 'custom',
surfaceKey: 'http-response-summary',
fallbackSurfaceId: 'default.json',
},
}

Set version to keep using one published version:

ui: {
output: {
kind: 'custom',
surfaceKey: 'http-response-summary',
version: 3,
fallbackSurfaceId: 'default.json',
},
}

fallbackSurfaceId identifies the first default candidate to try when the custom surface cannot be rendered. Other compatible defaults and the generic default are also returned to the UI.

Select a built-in default

No Surface resource is needed when a built-in is sufficient:

const saveResult = new Step({
id: 'save-result',
name: 'Save Result',
type: 'core.set',
params: {
values: { saved: true },
},
ui: {
output: {
kind: 'default',
surfaceId: 'default.json',
},
},
});

The current generic defaults are:

SlotSurface ID
outputdefault.json
waitdefault.resume-form

Node packages can register additional defaults for a node type.

Create a wait surface

Wait surfaces can collect form values and resume or stop the execution:

const approvalSurface = new Surface({
id: 'expense-approval',
name: 'Expense approval',
slot: 'wait',
definition: {
schema: 'kit.ui/v1',
root: 'form',
components: [
{
id: 'form',
type: 'Form',
children: ['amount', 'comment', 'decision'],
},
{
id: 'amount',
type: 'KeyValue',
props: {
label: 'Requested amount',
value: {
path: '$input/Collect Request/main/0/0/amount',
},
},
},
{
id: 'comment',
type: 'TextArea',
props: {
label: 'Reviewer comment',
bind: '/$form/comment',
},
},
{
id: 'decision',
type: 'ApproveRejectBar',
actions: {
approve: {
type: 'node.resume',
payload: {
comment: { path: '$form/comment', default: '' },
},
},
reject: {
type: 'node.stop',
payload: {
reason: { path: '$form/comment', default: 'Rejected by reviewer' },
},
},
},
},
],
form: {
fields: [
{
name: 'comment',
label: 'Reviewer comment',
type: 'textarea',
},
],
},
},
});

const approval = new Step({
id: 'manager-approval',
name: 'Manager Approval',
type: 'core.set',
params: {
values: { reviewer: 'manager' },
},
ui: {
wait: approvalSurface,
},
});

The Kit adds pauseAfterExecution: true during serialization. Explicitly setting it to false while assigning a wait surface is rejected.

Sync and deploy

Save the surface and its workflows in normal .culvii.ts entrypoints:

# Create session drafts and update them on save
culvii dev

# Publish changed surfaces, workflows, and agents together
culvii deploy --env sandbox --workspace my-workspace

Changing a published surface creates a new immutable surface version. The deploy plan includes the surface even when no workflow code changed, provided the surface is constructed by a discovered entrypoint.