For AI agents: an index of this documentation is at https://developer.rippling.com/llms.txt. Public pages have a Markdown version at the same URL with .md added.

Skip to main content
guide

Server round-trips

Call your Rippling function from a function page with triggerFunction and handle multiple actions in onRipplingEvent.

Use with AI:Rippling AI ChatGPT Claude Cursor

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​

ApproachWhen to useExample
Client-onlyTyping in a bound field, toggling visibility, local notifications$bindState('/name'), setState, showNotification
Server round-tripLogic that must run on the serverFetch 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:

Situationaction valueWhat to return
User opens the pageinitFull SDUI spec in body.spec (JSON string)
User triggers triggerFunctionThe 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​