> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thesys.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Implementing Custom Actions (Buttons)

> Connect C1-generated UI to your application's specific logic and workflows.

<Note>
  For a complete overview of the interactivity system, please read the **[Actions](/guides/interactivity)** guide.
</Note>

This guide focuses on **Custom Actions**, which allow you to go beyond the built-in behaviors and trigger your application's unique functions directly from the C1 UI. This makes the generative interface a true, interactive part of your application, enabling powerful workflows such as:

* Downloading a generated report.
* Opening a product-specific checkout modal.
* Triggering a function in your application, like creating a new project or sending an email.

### Implementing a Custom Action

Let's walk through an example of implementing a `download_report` custom action. It involves two steps: defining the action on your backend and handling it on your frontend.

#### 1. Define the Custom Action (Backend)

To make the LLM aware of your custom action, you must define its name and the parameters it accepts. This is done by passing a `c1_custom_actions` object within the `metadata` of your API call.

We recommend using a schema library (like Zod for TypeScript or Pydantic for Python) to define your action's parameters.

<CodeGroup dropdown>
  ```typescript theme={null}
  import { z } from "zod";
  import { zodToJsonSchema } from "zod-to-json-schema";
  import OpenAI from "openai";

  const client = new OpenAI({
    baseURL: "https://api.thesys.dev/v1/embed",
    apiKey: process.env.THESYS_API_KEY,
  });

  const messages = [
    { role: "system", content: "When the user asks to download data, offer them a 'download_report' button." },
    { role: "user", content: "Can I get a copy of our quarterly sales data?" }
  ];

  const response = await client.chat.completions.create({
    model: "c1/anthropic/claude-sonnet-4/v-20250930",
    messages: messages,
    metadata: {
      thesys: JSON.stringify({
        c1_custom_actions: {
          download_report: zodToJsonSchema(z.object({
            reportType: z.enum(["sales", "marketing", "inventory"]).describe("The category of the report."),
            format: z.enum(["csv", "pdf"]).default("pdf").describe("The file format for the download."),
            quarter: z.string().optional().describe("The specific quarter for the report, e.g., 'Q3 2025'."),
          })),
        }
      }),
    },
  });
  ```

  ```python theme={null}
  import os
  import json, jsonref
  import openai
  from pydantic import BaseModel, Field
  from typing import Literal, Optional

  client = openai.OpenAI(
      api_key=os.environ.get("THESYS_API_KEY"),
      base_url="https://api.thesys.dev/v1/embed",
  )

  # Define the parameters for the custom action using Pydantic
  class DownloadReportParams(BaseModel):
      reportType: Literal["sales", "marketing", "inventory"] = Field(..., description="The category of the report.")
      format: Literal["csv", "pdf"] = Field(default="pdf", description="The file format for the download.")
      quarter: Optional[str] = Field(default=None, description="The specific quarter for the report, e.g., 'Q3 2025'.")


  messages = [
      {"role": "system", "content": "When the user asks to download data, offer them a 'download_report' button."},
      {"role": "user", "content": "Can I get a copy of our quarterly sales data?"}
  ]

  def generate_schema(model: BaseModel):
    schema = model.model_json_schema()
    schema = jsonref.replace_refs(schema, proxies=False) # dereference $refs and $defs
    schema.pop("$defs", None) # remove $defs
    schema.pop("$ref", None) # remove $refs
    return schema

  # Construct the metadata payload
  thesys_metadata = {
      "c1_custom_actions": {
          "download_report": generate_schema(DownloadReportParams)
      }
  }

  response = client.chat.completions.create(
      model="c1/anthropic/claude-sonnet-4/v-20250930",
      messages=messages,
      metadata={"thesys": json.dumps(thesys_metadata)},
  )
  ```
</CodeGroup>

When the LLM generates a UI that includes a "Download Report" button, it will attach the action type `download_report` and the corresponding parameters to it.

#### 2. Handle the Custom Action (Frontend)

On the frontend, you use the same `onAction` callback from the core Actions guide. You simply add a new `case` to your `switch` statement to handle your custom action type.

**Using `<C1Component>`**

Your handler should check for the `download_report` action type and then trigger your application's logic, like opening a modal or starting a file download.

```tsx theme={null}
<C1Component
  c1Response={c1Response}
  isStreaming={isLoading}
  onAction={(event) => {
    switch (event.type) {
      // Your custom action case
      case "download_report":
        // Optionally add a message to the UI to confirm the action
        pushUserMessageToChat(`Downloading ${event.params.reportType} report...`);
        // Trigger your application's download logic with the action's parameters
        downloadReport(event.params);
        break;

      // Built-in action cases
      case "open_url":
        window.open(event.params.url, "_blank", "noopener,noreferrer");
        break;

      case "continue_conversation":
      default:
        const { llmFriendlyMessage, humanFriendlyMessage } = event.params;
        pushUserMessageToChat(humanFriendlyMessage);
        callApi(llmFriendlyMessage);
        break;
    }
  }}
/>
```

**Using `<C1Chat>`**

<Note>
  If you are using persistence with the `useThreadManager` hook, you need to pass the `onAction` callback directly to the hook.
</Note>

The `<C1Chat>` component handles built-in actions automatically, so its `onAction` prop is used exclusively for your custom actions.

<CodeGroup>
  ```tsx C1Chat theme={null}
  <C1Chat
    apiUrl="/api/chat"
    onAction={(event) => {
      // C1Chat handles 'continue_conversation' and 'open_url',
      // so you only need to handle your custom actions.
      switch (event.type) {
        case "download_report":
          // Trigger your application's logic
          downloadReport(event.params);
          break;
      }
    }}
  />
  ```

  ```tsx useThreadManager theme={null}
  import { useThreadManager, C1Chat } from "@thesysai/genui-sdk";

  const threadManager = useThreadManager(
    apiUrl="/api/chat",
    onAction={(event) => {
      switch (event.type) {
        case "download_report":
          // Trigger your application's logic
          downloadReport(event.params);
          break;
      }
    }}
  );
  <C1Chat
    threadManager={threadManager}
  />
  ```
</CodeGroup>
