# Server round-trips

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

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

## 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](https://developer.rippling.com/documentation/rippling-platform/sdui/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`) |

```typescript
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:

```typescript
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`

```typescript
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:

```typescript
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](https://developer.rippling.com/documentation/rippling-platform/data-model/object-types) or external API and load it on `init`.

* * *

## Complete example

```typescript
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](https://developer.rippling.com/documentation/rippling-platform/sdui/hello-world#step-3-attach-the-function-in-app-studio)).

* * *

## Next steps

-   Use `$bindState` and local actions when you do **not** need the server—see [State & Data Binding](https://developer.rippling.com/documentation/rippling-platform/sdui/state-data-binding).
-   Look up `triggerFunction` parameters in [Reference](https://developer.rippling.com/documentation/rippling-platform/sdui/reference#available-actions).
-   Browse button and text props in the [component catalog](https://developer.rippling.com/documentation/rippling-platform/sdui/components).

Source: https://developer.rippling.com/documentation/rippling-platform/sdui/server-round-trips

> For the complete documentation index, see [llms.txt](https://developer.rippling.com/llms.txt). Public pages have a Markdown version at the same URL with .md added.
