Skip to main content
For a complete overview of the interactivity system, please read the Actions guide.
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.
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'."),
        })),
      }
    }),
  },
});
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.
<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, userFriendlyMessage } = event.params;
        pushUserMessageToChat(userFriendlyMessage);
        callApi(llmFriendlyMessage);
        break;
    }
  }}
/>
Using <C1Chat> The <C1Chat> component handles built-in actions automatically, so its onAction prop is used exclusively for your custom actions.
<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;
    }
  }}
/>
If you are using persistence with the useThreadManager hook, you can pass the onAction callback directly to the hook.
I