Overview
A server round-trip is when the function page calls your Rippling function again after the initial load. The first call happens on page open (action: 'init') and returns the full SDUI spec. Later calls run when a user action needs server logic—loading data, calculating a value, or saving a record.
Many interactions stay on the client ($bindState, setState, showNotification) and never hit your function. Use server round-trips when the browser cannot do the work alone.
This guide shows the pattern with a small counter: a button calls your function, the server returns a new count, and the UI updates. The focus is the request/response loop, not the counter itself.
Complete Hello World first, so you already return a spec on init.
Client-only vs server round-trip
| Approach | When to use | Example |
|---|---|---|
| Client-only | Typing in a bound field, toggling visibility, local notifications | $bindState('/name'), setState, showNotification |
| Server round-trip | Logic that must run on the server | Fetch records, authorize an action, compute from external data |
Server round-trips use the catalog action triggerFunction on a component's on handler (usually a button press).
Handler contract
Your function branches on action from event.parameters:
| Situation | action value | What to return |
|---|---|---|
| User opens the page | init | Full SDUI spec in body.spec (JSON string) |
User triggers triggerFunction | The required inputs.action value (e.g. increment) | Data to write into state (see responsePath) |
export async function onRipplingEvent(
event: FunctionEvent,
context: FunctionContext,
): Promise<FunctionResponse> {
const params = (event.parameters ?? {}) as Record<string, string | undefined>;
const action = params.action;
if (action === 'init') {
// Return SDUI spec
}
if (action === 'increment') {
// Return new value for responsePath
}
}
Step 1: Return a spec with state on init
On init, build a spec that displays a count and a button. Use createSpec so $state and $template work in the spec:
import { createSpec } from '@rippling/pebble-sdui-web';
import {
Button,
Heading,
Text,
VStack,
render,
triggerFunction,
} from '@rippling/pebble-sdui-web/catalogs/custom-apps';
import { FunctionContext } from '@rippling/rippling-sdk';
interface CounterState {
count: number;
}
function buildSpec(context: FunctionContext) {
const state: CounterState = { count: 0 };
const { $state, $template } = createSpec<CounterState>({ state });
const functionApiName = context.function.function_api_name;
const root = VStack({
props: { gap: 'space300', align: 'center' },
children: [
Heading({ props: { level: 2, text: 'Counter' } }),
Text({
props: {
text: $template`Count is ${$state('/count')}`,
typestyle: 'typestyleBodyLarge',
},
}),
Button({
props: { label: 'Increment', appearance: 'PRIMARY' },
on: {
press: triggerFunction({
functionApiName,
inputs: {
action: 'increment',
current: $state('/count'),
},
responsePath: '/count',
}),
},
}),
],
});
return render({ root, state });
}
functionApiName — Tells triggerFunction which handler to invoke (this function).
inputs — Parameters sent to onRipplingEvent on the next round-trip. They must include an action string. Your function is stateless between calls, so send any additional values the server needs (here, the current count).
responsePath — Where to write the function response in page state. When the server returns a new count, the client updates /count and the label re-renders.
Step 2: Handle init in onRipplingEvent
if (action === 'init') {
return new FunctionResponse({
body: { spec: JSON.stringify(buildSpec(context)) },
statusCode: 200,
headers: {},
message: '',
});
}
Step 3: Handle the button action
When the user presses the button, Rippling calls your function with action: 'increment' and the current input from the spec:
if (action === 'increment') {
const current = Number(params.current ?? 0);
return new FunctionResponse({
body: (current + 1) as unknown as Record<string, unknown>,
statusCode: 200,
headers: {},
message: '',
});
}
The renderer writes the response body to responsePath (/count). You do not return a new full spec on this action—only the data to patch.
Your function does not persist state between calls. To survive a reload or share data across users, store it in a custom object or external API and load it on init.
Complete example
import { createSpec } from '@rippling/pebble-sdui-web';
import {
Button,
Heading,
Text,
VStack,
render,
triggerFunction,
} from '@rippling/pebble-sdui-web/catalogs/custom-apps';
import { FunctionContext, FunctionEvent, FunctionResponse } from '@rippling/rippling-sdk';
interface CounterState {
count: number;
}
function buildSpec(context: FunctionContext) {
const state: CounterState = { count: 0 };
const { $state, $template } = createSpec<CounterState>({ state });
const functionApiName = context.function.function_api_name;
const root = VStack({
props: { gap: 'space300', align: 'center' },
children: [
Heading({ props: { level: 2, text: 'Counter' } }),
Text({
props: {
text: $template`Count is ${$state('/count')}`,
typestyle: 'typestyleBodyLarge',
},
}),
Button({
props: { label: 'Increment', appearance: 'PRIMARY' },
on: {
press: triggerFunction({
functionApiName,
inputs: {
action: 'increment',
current: $state('/count'),
},
responsePath: '/count',
}),
},
}),
],
});
return render({ root, state });
}
export async function onRipplingEvent(
event: FunctionEvent,
context: FunctionContext,
): Promise<FunctionResponse> {
const params = (event.parameters ?? {}) as Record<string, string | undefined>;
const action = params.action;
if (action === 'init') {
return new FunctionResponse({
body: { spec: JSON.stringify(buildSpec(context)) },
statusCode: 200,
headers: {},
message: '',
});
}
if (action === 'increment') {
const current = Number(params.current ?? 0);
return new FunctionResponse({
body: (current + 1) as unknown as Record<string, unknown>,
statusCode: 200,
headers: {},
message: '',
});
}
return new FunctionResponse({
body: {},
statusCode: 400,
headers: {},
message: 'Unknown action',
});
}
Attach the function to a function page in App Studio (see Hello World).
Next steps
- Use
$bindStateand local actions when you do not need the server—see State & Data Binding. - Look up
triggerFunctionparameters in Reference. - Browse button and text props in the component catalog.