---
title: "Functions and states"
description: "Widget functions respond to visitor selections, and states control which sections appear. Reference function settings, placeholders, saved variables, and condition operators."
---

:::note[Plan]
Actions are available on the **Starter** plan and above.
:::

A function is a named behavior that runs when a visitor selects a button or list item. A state is a named section that appears when its visibility rules allow it. Configure both in the [widget builder](/docs/widgets/building-widgets).

## Function types

In **Functions**, select **Add your first function**, or **Add function** when functions already exist. Each function needs a unique **Function name** and a **Function type**. Names match `^[A-Za-z_][A-Za-z0-9_]{0,63}$`, such as `pickProduct`.

| Function type | Setting | Description |
| --- | --- | --- |
| **Open link** | **URL** | Opens an HTTP or HTTPS URL in a new tab. Example: `https://example.com/orders/{{order.number}}`. Other URL schemes are refused. |
| **Send message** | **Message** | Sends text into the conversation as the visitor. Example: `Tell me about {{productName}}`. Empty messages after substitution and trimming do not send. In the turn it starts, the action that showed the widget does not run again, so a button such as **Book 9:30 AM** cannot reopen the same card. |
| **Set variables** | **Variables** | Updates local widget variables. Example: key `pickedName`, value `{{productName}}`. |
| **Dismiss widget** | No extra setting | Hides this widget. |
| **Call API** | API connection and follow-ups | Sends a request through a saved [API connection](/docs/agents/api-connections) and updates widget state using the response. |

Functions do not run while the conversation is busy.

## Call API

**Call API** functions send HTTP requests from Chatvox to your backend through a saved API connection when a visitor selects a button or list item. They cost no credits and do not invoke an LLM. Only one Call API function can run at a time per reply widget.

:::warning
Variables and inputs originate from the visitor. Only built-in tokens such as `{{conversation.id}}` and `{{contact.*}}` (from verified contacts) are trustworthy for identity. Never trust visitor-supplied variables or inputs to identify an account.
:::

### Configuration

A **Call API** function includes:

- **Function name**: unique identifier matching `^[A-Za-z_][A-Za-z0-9_]{0,63}$`.
- **Additional inputs**: up to 10 keys passed when the button is clicked (such as `slot`).
- **API connection**: choose a saved connection for this agent, or create one from the function editor. It supplies the base URL, authentication, and headers.
- **Method**: `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`.
- **Path and query**: the endpoint path, such as `/bookings/{{order_id}}`. The request uses the connection's base URL. The base URL and final request must use HTTPS and reach a public address.
- **Send fields as**: **JSON body** or **Query params**. `GET` requests always send declared inputs as query parameters.
- **JSON body (optional)**: a JSON template for methods other than `GET`. Use placeholders such as `{{appointmentSlot}}` to include widget data or input values. When you set a template, it defines the JSON body; Chatvox does not add declared inputs to it automatically. If **Send fields as** is **Query params**, declared inputs still go in the URL when you set a JSON body.
- **Return schema**: up to 20 return fields mapping a field key to a dot path in the JSON response (for example, `bookingRef` ← `data.reference`).
- **Success path (optional)**: dot path in the JSON response that must contain a value Chatvox parses as true, such as `true`, `1`, or `"yes"`, for the call to count as a success. If omitted, any 2xx HTTP response status is considered successful.
- **Timeout (seconds)**: request timeout from 1 to 15 seconds (default 8 seconds).
- **Loading**: visual feedback while the request runs:
  - **None**: buttons are disabled only.
  - **Button**: the clicked `Button` or `ListViewItem` shows an inline spinner.
  - **Whole widget**: a semi-transparent overlay with a spinner covers the card.

### Tokens and precedence

The path, connection header values, and request body support double-brace tokens such as `{{date}}`. Values resolve with the following precedence (highest precedence wins):

1. **Built-ins**: `conversation.id`, `contact.*` (verified contact details), `page.url`, and `utm.*`. These values come from the conversation; contact values are available for verified contacts.
2. **Declared inputs**: values passed in `additionalInputs` matching declared input keys.
3. **Variables**: current visitor widget variables (including values from form inputs).
4. **Data**: the widget's initial reply data from message metadata.

Tokens in the path are URL-encoded. JSON body templates are decoded and re-encoded, preserving a value's type when it consists of one placeholder.

### Follow-ups

**Call API** functions use lifecycle follow-ups instead of a single **After it runs** setting:

- **On start**: runs locally as soon as the visitor clicks (**None**, **Dismiss widget**, or **Set variables**).
- **On success**: runs when the request returns a 2xx status and, if defined, its **Success path** parses as true. Values in **Set variables** can read returned fields, such as `{{bookingRef}}`.
- **On failure**: runs when the request fails, times out, or returns a non-success response (**None**, **Dismiss widget**, or **Set variables**). If **On failure** is set to **None**, an inline alert displays: `Couldn’t complete that. Try again.`

State changes are saved to the server in a single save after the request settles. If a visitor reloads during an in-flight request, the card restores to its pre-click state.

### Testing in the builder

The widget builder provides two tools for Call API functions. Both send a real request to your API:

- **Run**: in the function editor, enter test values for declared inputs and select **Run**. Chatvox sends a test request and displays the status badge, response time, extracted return fields, and raw JSON response.
- **Populate from API**: select **Populate from API** to run the request and inspect leaf paths in the JSON response. Selecting paths adds them directly to your return schema without typing.
- **Preview result toggle**: in the preview pane, use the **Call API result: Success | Failure** toggle to simulate both success and failure flows without calling your backend.

### Follow-ups for other functions

**After it runs** chooses one follow-up:

| Setting | Description |
| --- | --- |
| **None** | No further change. |
| **Dismiss widget** | Hides the widget after the main behavior. |
| **Set variables** | Applies **Variables after the action** using the same value rules as Set variables. |

For example, a **Send message** function with **Message** set to `I choose {{productName}}` and **After it runs** set to **Dismiss widget** sends the selection and hides the card. An invalid link or empty message stops before its follow-up. Follow-ups cannot call another named function.

Follow-up placeholders use the original click scope. They do not read variables newly assigned by the main function during that same click.

## Call a function from code

Use `onClickAction` on a `Button` or `ListViewItem`. Its `functionName` must be a literal string naming a function you defined. `additionalInputs` is an optional object of values for that click.

```jsx
<Card>
  <Button
    label="Choose this product"
    onClickAction={{
      functionName: "pickProduct",
      additionalInputs: { productName: product.name },
    }}
  />
</Card>
```

Declare `product.name` as a widget field. Define `pickProduct` in **Functions**, for example as **Send message** with **Message** set to `I choose {{productName}}`.

In a loop, additional inputs can read the current item:

```jsx
<Card>
  <ListView>
    {products.map((product) => (
      <ListViewItem
        onClickAction={{
          functionName: "pickProduct",
          additionalInputs: { productName: product.name },
        }}
      >
        <Text value={product.name} />
      </ListViewItem>
    ))}
  </ListView>
</Card>
```

Declare `products` as a **List** field with visitor-safe items. Click inputs are not saved automatically. Use **Set variables** to retain a selected value.

## Placeholders

Open link URLs, messages, and Set variables values support double-brace placeholders such as `{{order.number}}`. These templates are separate from JSX expressions such as `{order.number}` in code.

| Source | Example |
| --- | --- |
| Widget data | `{{order.number}}` |
| Saved widget variables | `{{pickedName}}` |
| Current loop item and optional index | `{{product.name}}`, `{{index}}` when those map parameters exist |
| Additional inputs for the click | `{{productName}}` |

Variables override widget data with the same key. Loop parameters override matching keys inside their loop. Additional inputs override matching keys for the function call.

Paths contain letters, digits, underscores, and dots. Use `{{products.0.name}}` for a numeric segment. Bracket notation such as `{{products[0].name}}` is not supported in placeholders. Prototype-related path segments are blocked. Open link, message, and Set variables placeholders read widget data, saved variables, loop values, and click inputs. **Call API** functions can also use the built-in conversation values listed above; they do not expose API credentials to the widget.

In Open link URLs and messages, values become text. Missing or null values become empty text. Open link values are not URL-encoded, so use data that is safe for the destination URL. For example, with `{"order":{"number":"A-1234"}}`, `Order {{order.number}}` becomes `Order A-1234`.

## Set variables rules

| Setting | Rule |
| --- | --- |
| **Variable name** | Matches `^[A-Za-z_][A-Za-z0-9_]{0,63}$`. Dot paths are not allowed. |
| Reserved names | `__proto__`, `prototype`, and `constructor` are refused. |
| **Variable value** | A text template of up to 1,000 characters, or null. |
| Accepted result | Text, a finite number, a boolean, or null. Objects, lists, missing values, and non-finite numbers are not assigned. |

A value containing only one placeholder preserves its underlying type. For example, `{{quantity}}` preserves a numeric quantity, while `Quantity: {{quantity}}` produces text. Entering `true` directly in the value input produces the text `"true"`, not a boolean. A null template assigns null; a missing single-placeholder value leaves the variable unchanged.

Assignments use the original click values. An earlier assignment does not feed a later assignment in the same call.

Variables and dismissal are saved with the widget on its specific reply and restored when the conversation reloads. They do not change another reply’s card or the widget definition. Sent messages remain visitor messages. The reply retains its original widget definition and data after action or widget edits.

## State tags

In **States**, select **Add your first state**, or **Add state** when states already exist. Each **State name** must be unique and match `^[A-Z][A-Za-z0-9]{0,39}$`. Use a distinct name such as `Picked`, then wrap components in a tag with that exact name. State tags have no props.

```jsx
<Card>
  <Picked>
    <Text value="You have selected a product." />
  </Picked>
</Card>
```

| Visibility | Behavior |
| --- | --- |
| **Always visible** | Shows the children regardless of conditions. |
| **Off** | Turns the state off without deleting it. The children never show, and the conditions stay saved. |
| **Conditional** | Shows the children only when condition groups match. No groups, or a group with no conditions, evaluates to false. |

Conditions read widget data and variables by dot path, such as `order.status` or `pickedName`. Variables override data fields with the same key. Inside loops, states can also read the current item’s scope.

## Condition operators

Under **Show when**, enter a **Condition field**, select a **Condition operator**, and enter a comparison value when needed. Comparison values are text. Matches are case-sensitive.

| Operator | Exact behavior |
| --- | --- |
| **Equals** | Converts the field value to text and compares it with the expected text. Missing and null become empty text. The number `3` equals the text `3`. |
| **Does not equal** | The inverse of Equals. |
| **Contains** | For text, checks for a substring. For a list, checks whether any item converted to text equals the expected text, treating null as empty text. Other types do not match. |
| **Does not contain** | The inverse of Contains, including true for unsupported field types. |
| **Greater than** | Converts both values to numbers and checks a strict greater-than comparison. |
| **Less than** | Converts both values to numbers and checks a strict less-than comparison. |
| **Is empty** | Matches missing values, null, empty text, an empty list, or an object with no keys. Zero, false, and whitespace-only text are not empty. |
| **Is not empty** | The inverse of Is empty. |

Numeric conversion follows JavaScript `Number`: empty text and null become zero, booleans become zero or one, and missing or nonnumeric text becomes `NaN`. Comparisons involving `NaN` are false. **Contains** with empty expected text matches every text value.

For example, `order.status` **Equals** `Shipped` matches `"Shipped"` but not `"shipped"`. `tags` **Contains** `sale` matches `["sale", "new"]`, but not `["wholesale"]`.

### Conditions and groups

**And** requires both the result so far and the next condition to match. **Or** requires either to match. Conditions combine left to right, without normal expression precedence. Groups evaluate separately, then their results also combine left to right. The first condition and first group do not need a connector.

For example, `A Or B And C` means `(A Or B) And C`. To express `A Or (B And C)`, put A in one group and B **And** C in a second group. Use **Or** as the second group’s connector.

## Combined example: show a picked state

Declare `product.name` as a **Text** field and use this default data:

```json
{
  "product": { "name": "Canvas bag" }
}
```

Configure the function:

| Setting | Value |
| --- | --- |
| **Function name** | `pickProduct` |
| **Function type** | **Set variables** |
| **Variable name** | `pickedName` |
| **Variable value** | `{{productName}}` |
| **After it runs** | **None** |

Configure the state:

| Setting | Value |
| --- | --- |
| **State name** | `Picked` |
| **Visibility** | **Conditional** |
| **Condition field** | `pickedName` |
| **Condition operator** | **Is not empty** |

Use this code:

```jsx
<Card>
  <Title value={product.name} />
  <Button
    label="Choose product"
    onClickAction={{
      functionName: "pickProduct",
      additionalInputs: { productName: product.name },
    }}
  />
  <Picked>
    <Text value={`You picked ${pickedName}`} />
  </Picked>
</Card>
```

Before selection, `pickedName` is missing and **Picked** stays hidden. Selecting **Choose product** saves `Canvas bag` to `pickedName`, so the state appears. The selection survives a conversation reload. In the builder preview, **Reset** clears it.

## Walkthrough: Book an appointment without a second action

This walkthrough converts the **Appointment slots** template from sending a chat message to booking directly through an API using a **Call API** function.

1. **Create the widget from the template**

    Go to **Build** > **Widgets** and select **Create widget**. In the template gallery, select **Appointment slots**, then select **Use template**.

    Go to **Fields** and add two fields:

    | Field | Type | Description |
    | --- | --- | --- |
    | `status` | **Text** | Booking status (`booking`, `booked`, or `error`). |
    | `bookingRef` | **Text** | Confirmation reference returned by the API. |

    In **Default**, update the JSON example to include initial empty values:

    ```json
    {
      "date": "Monday, October 19",
      "slots": [
        { "label": "9:30 AM" },
        { "label": "11:00 AM" },
        { "label": "1:30 PM" },
        { "label": "3:00 PM" }
      ],
      "picked": "",
      "status": "",
      "bookingRef": ""
    }
    ```

2. **Define states for booked and failed views**

    Go to **States** and add two conditional states:

    | State name | Visibility | Condition field | Operator | Value |
    | --- | --- | --- | --- | --- |
    | `Booked` | **Conditional** | `status` | **Equals** | `booked` |
    | `BookingFailed` | **Conditional** | `status` | **Equals** | `error` |

3. **Configure the Call API function**

    Go to **Build** > **API connections** and create a connection named `Bookings` with base URL `https://api.example.com`. Then return to **Functions** and open `bookSlot`. Change its **Function type** from **Send message** to **Call API**, and select the `Bookings` connection.

    Configure the function settings:

    - **Method**: `POST`
    - **Path and query**: `/bookings`
    - **Request body (optional)**:

    ```json
    {
      "date": "{{date}}",
      "time": "{{picked}}",
      "conversation_id": "{{conversation.id}}",
      "customer_email": "{{contact.email}}"
    }
    ```

    - **Return schema**: add a row with field key `bookingRef` and response path `data.reference`.
    - **Loading**: select **Button**.
    - **On start**: select **Set variables**, with variable `status` set to `booking`.
    - **On success**: select **Set variables**, with variable `status` set to `booked`, and variable `bookingRef` set to `{{bookingRef}}`.
    - **On failure**: select **Set variables**, with variable `status` set to `error`.

      </Step>
      <Step title="Update widget code">

    Go to **Code** and update the markup so the card handles the booking states:

    ```jsx
    <Card size="md" status={{ text: "Book a call", icon: "calendar" }}>
      <Col gap={1}>
        <Title value="Pick a time" size="md" />
        <Caption value={date} />
      </Col>
      <Row gap={2} justify="between" wrap={true}>
        {slots.map((slot) => (
          <Col width="48%">
            <Button
              label={slot.label}
              variant={picked === slot.label ? "solid" : "outline"}
              size="md"
              block={true}
              iconStart={picked === slot.label ? "check" : ""}
              onClickAction={{ functionName: "pickSlot", additionalInputs: { slot: slot.label } }}
            />
          </Col>
        ))}
      </Row>
      <Picked>
        <Divider />
        <Row gap={2} align="center">
          <Icon name="check-circle" color="success" />
          <Text value={`${date} at ${picked}`} weight="medium" />
        </Row>
        <Button
          label="Confirm booking"
          variant="solid"
          size="md"
          block={true}
          iconStart="calendar"
          onClickAction={{ functionName: "bookSlot" }}
        />
      </Picked>
      <Booked>
        <Divider />
        <Box border={true} radius="lg" padding={3} gap={2} align="center">
          <Icon name="check-circle" color="success" size={24} />
          <Text value={`Booked for ${date} at ${picked}`} weight="semibold" />
          <Caption value={`Reference: ${bookingRef}`} />
        </Box>
      </Booked>
      <BookingFailed>
        <Divider />
        <Box border="error" radius="lg" padding={3} gap={1} align="center">
          <Icon name="triangle-alert" color="error" />
          <Text value="Could not complete booking. Please try another time." />
        </Box>
      </BookingFailed>
    </Card>
    ```

    In the preview header, toggle **Call API result** between **Success** and **Failure** to verify both outcomes. Save the widget.
