An action can do one of the following things:

  • Execute a function on the server (most common)
  • If block is followed by a streamable variable, stream the variable on the client. Otherwise, execute a function on the server.
  • Execute a function on the client
  • Display a custom embed bubble

Server function

The most common action is to execute a function on the server. This is done by simply declaring that function in the action block.

Example:

export const sendMessage = createAction({
  // ...
  run: {
    server: async ({
      credentials: { apiKey },
      options: { botId, message, responseMapping, threadId },
      variables,
      logs,
    }) => {
      const res: ChatNodeResponse = await got
        .post(apiBaseUrl + botId, {
          headers: {
            Authorization: `Bearer ${apiKey}`,
          },
          json: {
            message,
            chat_session_id: isEmpty(threadId) ? undefined : threadId,
          },
        })
        .json()

      if (res.error)
        logs.add({
          status: 'error',
          description: res.error,
        })

      if (res.error)
        logs.responseMapping?.forEach((mapping) => {
          if (!mapping.variableId) return

          const item = mapping.item ?? 'Message'
          if (item === 'Message') variables.set(mapping.variableId, res.message)

          if (item === 'Thread ID')
            variables.set(mapping.variableId, res.chat_session_id)
        })
    },
  },
})

As you can see the function takes credentials, options, variables and logs as arguments.

The credentials are the credentials that the user has entered in the credentials block.

The options are the options that the user has entered in the options block.

The variables object contains helper to save and get variables if necessary.

The logs allows you to log anything during the function execution. These logs will be displayed as toast in the preview mode or in the Results tab on production.

Server function + stream

If your block can stream a message (like OpenAI), you can implement a stream object to handle it.

export const createChatCompletion = createAction({
  // ...
  run: {
    server: async (params) => {
      //...
    },
    stream: {
      getStreamVariableId: (options) =>
        options.responseMapping?.find(
          (res) => res.item === 'Message content' || !res.item
        )?.variableId,
      run: async ({ credentials: { apiKey }, options, variables }) => {
        const config = {
          apiKey,
          baseURL: options.baseUrl,
          defaultHeaders: {
            'api-key': apiKey,
          },
          defaultQuery: options.apiVersion
            ? {
                'api-version': options.apiVersion,
              }
            : undefined,
        } satisfies ClientOptions

        const openai = new OpenAI(config)

        const response = await openai.chat.completions.create({
          model: options.model ?? defaultOpenAIOptions.model,
          temperature: options.temperature
            ? Number(options.temperature)
            : undefined,
          stream: true,
          messages: parseChatCompletionMessages({ options, variables }),
        })

        return OpenAIStream(response)
      },
    },
  },
})

The getStreamVariableId function defines which variable should be streamed.

The run works the same as the server function. It needs to return Promise<ReadableStream<any> | undefined>.

Client function

If you want to execute a function on the client instead of the server, you can use the client object.

This makes your block only compatible with the Web runtime. It won’t work on WhatsApp for example, the block will simply be skipped.

Example:

export const shoutName = createAction({
  // ...
  run: {
    web: ({ options }) => {
      return {
        args: {
          name: options.name ?? null,
        },
        content: `alert('Hello ' + name)`,
      }
    },
  },
})

The web function needs to return an object with args and content.

args is an object with arguments that are passed to the content context. Note that the arguments can’t be undefined. If you want to pass an not defined argument, you need to pass null instead.

content is the code that will be executed on the client. It can call the arguments passed in args.

Display embed bubble

If you want to display a custom embed bubble, you can use the displayEmbedBubble object. See Cal.com block as an example.

Example:

export const bookEvent = createAction({
  // ...
  run: {
    web: {
      displayEmbedBubble: {
        parseInitFunction: ({ options }) => {
          if (!options.link) throw new Error('Missing link')
          const baseUrl = options.baseUrl ?? defaultBaseUrl
          const link = options.link?.startsWith('http')
            ? options.link.replace(/http.+:\/\/[^\/]+\//, '')
            : options.link
          return {
            args: {
              baseUrl,
              link: link ?? '',
              name: options.name ?? null,
              email: options.email ?? null,
              layout: parseLayoutAttr(options.layout),
            },
            content: `(function (C, A, L) {
                let p = function (a, ar) {
                  a.q.push(ar);
                };
                let d = C.document;
                C.Cal =
                  C.Cal ||
                  function () {
                    let cal = C.Cal;
                    let ar = arguments;
                    if (!cal.loaded) {
                      cal.ns = {};
                      cal.q = cal.q || [];
                      d.head.appendChild(d.createElement("script")).src = A;
                      cal.loaded = true;
                    }
                    if (ar[0] === L) {
                      const api = function () {
                        p(api, arguments);
                      };
                      const namespace = ar[1];
                      api.q = api.q || [];
                      typeof namespace === "string"
                        ? (cal.ns[namespace] = api) && p(api, ar)
                        : p(cal, ar);
                      return;
                    }
                    p(cal, ar);
                  };
              })(window, baseUrl + "/embed/embed.js", "init");
              Cal("init", { origin: baseUrl });

              Cal("inline", {
                elementOrSelector: typebotElement,
                calLink: link,
                layout,
                config: {
                  name: name ?? undefined,
                  email: email ?? undefined,
                }
              });

              Cal("ui", {"hideEventTypeDetails":false,layout});`,
          }
        },
      },
      waitForEvent: {
        getSaveVariableId: ({ saveBookedDateInVariableId }) =>
          saveBookedDateInVariableId,
        parseFunction: () => {
          return {
            args: {},
            content: `Cal("on", {
                action: "bookingSuccessful",
                callback: (e) => {
                  continueFlow(e.detail.data.date)
                }
              })`,
          }
        },
      },
    },
  },
})

The displayEmbedBubble accepts a parseInitFunction function. This function needs to return the same object as the web function. The function content can use the typebotElement variable to get the DOM element where the block is rendered.

Optionally you can also define a waitForEvent object. This object accepts a getSaveVariableId function that returns the variable id where the event data should be saved. It also accepts a parseFunction function that returns the same object as the web function. The function content can use the continueFlow function to continue the flow with the event data.