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

# Implementing Human in the loop (HITL) in an integration

The Human in the loop (HITL) interface allows you to implement human agent intervention in your integration.

## Terminology

Throughout this document, we will use the following terms:

<dl>
  <dt>
    <dfn id="dfn-integration" />
  </dt>

  <dd>The code that connects Botpress to an external service.</dd>

  <dt>
    <dfn id="dfn-external-service" />
  </dt>

  <dd>The service that provides HITL functionality. This could be a help desk system like Zendesk, or any other system that allows human agents to send and receive messages to end users.</dd>

  <dt>
    <dfn id="dfn-human-agent" />
  </dt>

  <dd>A person who interacts with end users through the external service. This could be a support agent, a sales representative, or any other type of human agent.</dd>

  <dt>
    <dfn id="dfn-end-user" />
  </dt>

  <dd>A person who interacts with your bot through Botpress. This could be a customer, a user, an employee, or any other type of end user.</dd>

  <dt>
    <dfn id="dfn-external-user" />
  </dt>

  <dd>A representation of an end user within the external service. This is typically created when a HITL session is started.</dd>

  <dt>
    <dfn id="dfn-hitl-session" />
  </dt>

  <dd>A conversation between an end user and a human agent. This is typically represented as a *ticket* in the external service.</dd>

  <dt>
    <dfn id="dfn-hitl-interface" />
  </dt>

  <dd>The interface that defines the contract for implementing HITL functionality in your integration. This interface specifies the actions, events, and channels that your integration must implement to support HITL.</dd>

  <dt>
    <dfn id="dfn-hitl-plugin" />
  </dt>

  <dd>The Botpress plugin that manages HITL sessions and relays messages between end users and your integration. Installing this plugin in a bot enables HITL functionality using the selected integration.</dd>

  <dt>
    <dfn id="dfn-entity" />
  </dt>

  <dd>In the context of the HITL interface and plugin, an entity can be provided in order to support extra parameters in the `startHitl` action. This is useful to provide extra information about the HITL session, such as a ticket priority or customer identification number.</dd>
</dl>

## External service requirements

The  providing HITL functionality **must** support the following:

* An API that allows creating .
* An API that allows creating .
* An API that allows adding messages to .
* An API that allows closing .
* Webhooks that can notify your  of the following events:
  * &#x20;closure.
  * &#x20;assignment.
  * &#x20;reply.

## Updating your `package.json` file

### Finding the current interface version

The current version of the `hitl` interface is:&#x20;

You will need this version number for the next steps.

### Adding the interface as a dependency

Once you have the  version, you can add it as a dependency to your :

**1. Open the package.json file**

Open your 's `package.json` file.

**2. Add the dependencies section**

If there is no `bpDependencies` section in your 's `package.json` file, create one:

```json package.json {2} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
{
  "bpDependencies": {}
}
```

**3. Add the interface as a dependency**

In the `bpDependencies` section, add the  as a dependency. For example, for version `2.0.0`, you would add the following:

```json package.json {3} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
{
  "bpDependencies": {
    "hitl": "interface:hitl@2.0.0"
  }
}
```

> [!WARNING]
>
> It's very important to follow this syntax: \\
>
> `"<interface-name>": "interface:<interface-name>@<version>"`.

**4. Save the package.json file**

Save the `package.json` file.

**5. Install the interface**

Now that you have added the  as a dependency, you can run the [`bp add`](https://botpress.com/docs/integrations/sdk/cli-reference#add) command to install it. This command will:

* Download the interface from Botpress.
* Install it in a directory named `bp_modules` in your 's root directory.

### Adding a helper build script

To keep your  up to date, we recommend adding a helper build script to your `package.json` file:

**1. Open the package.json file**

Open your 's `package.json` file.

**2. Add the build script**

In the `scripts` section, add the following script:

```json package.json {3} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
{
  "scripts": {
    "build": "bp add -y && bp build"
  }
}
```

> [!NOTE]
>
> If the `build` script already exists in your `package.json` file, please replace it.

**3. Save the package.json file**

Save the `package.json` file.

Now, whenever you run `npm run build`, it will automatically install the  and build your .

## Editing your integration definition file

### Adding the interface to your integration definition file

Now that the  is installed, you must add it your integration definition file in order to implement it.

**1. Open the integration.definition.ts file**

Open your 's `integration.definition.ts` file.

**2. Import the interface**

At the top of the file, import the :

```typescript integration.definition.ts theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
import hitl from './bp_modules/hitl'
```

**3. Add an empty entity**

In your `new IntegrationDefinition()` statement, add an empty entity:

```typescript integration.definition.ts {2-6} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new sdk.IntegrationDefinition({
  entities: {
    ticket: {                     // <= name of the entity
      schema: sdk.z.object({})
    },
  },
})
```

> [!NOTE]
>
> The name of the entity isn't important, but it's recommended to use a name that matches the terminology of the . For example, if the  uses the term "ticket" to refer to a , you could name the entity `ticket` or `hitlTicket`.

**4. Extend your definition**

Use the `.extend()` function at the end of your `new IntegrationDefinition()` statement:

```typescript integration.definition.ts {8-12} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new sdk.IntegrationDefinition({
  entities: {
    ticket: {                     // <= name of the entity
      schema: sdk.z.object({})
    },
  },
})
  .extend(hitl, (self) => ({
    entities: {
      hitlSession: self.entities.ticket, // <= name of the entity
    },
  }))
```

The exact syntax of `.extend()` will be explained in the next section.

### Configuring the interface

The `.extend()` function takes two arguments:

* The first argument is a reference to the interface you want to implement. In this case, it's `hitl`.
* The second argument is a configuration object. Using this object, you can override interface defaults with custom names, titles, and descriptions.

> [!TIP]
>
> Whilst renaming actions, events and channels is optional, it's highly recommended to rename these to match the terminology of the . This will help you avoid confusion and make your  easier to understand.

#### Renaming actions

The `hitl` interface defines three actions that are used to interact with the :

* `createUser` - Used by the  to request the creation of a user in the  and on Botpress.
* `startHitl` - Used by the  to request the creation of a  in the .
* `stopHitl` - Used by the  to request the closure of a  in the .

If you want to rename these actions, you can do so in the configuration object. For example, if you want to rename `createUser` to `hitlCreateUser`, you can do it like this:

```typescript integration.definition.ts {4} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
.extend(hitl, () => ({
  actions: {
    createUser: {
      name: 'hitlCreateUser',
    },
  },
}))
```

> [!TIP]
>
> For example, if you're using a *help desk* system like Zendesk, Jira Service Desk, or Freshdesk for HITL functionality, you might rename `startHitl` to `createTicket` and `stopHitl` to `closeTicket`. These systems use tickets to represent help requests, so renaming actions to match their terminology makes your  clearer and easier to understand.

#### Renaming events

The `hitl` interface defines these events to notify the plugin of changes in the external service:

* `hitlAssigned` - Emitted by your  to notify the  that a  has been assigned to a .
* `hitlStopped` - Emitted by your  to notify the  that a  has been closed.

If you want to rename these events, you can do so in the configuration object. For example, if you want to rename `hitlAssigned` to `agentAssigned`, you can do it like this:

```typescript integration.definition.ts {4} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
.extend(hitl, () => ({
  events: {
    hitlAssigned: {
      name: 'agentAssigned',
    },
  },
}))
```

#### Renaming channels

The `hitl` interface defines these channels:

* `hitl` - Used by the  to send and receive messages from the . This represents the communication channel for the , like a support ticket on Zendesk or a direct message thread on Slack.

If you want to rename this channel, you can do so in the configuration object. For example, if you want to rename `hitl` to `supportTicket`, you can do it like this:

```typescript integration.definition.ts {4} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
.extend(hitl, () => ({
  channels: {
    hitl: {
      name: 'supportTicket',
    },
  },
}))
```

## Implementing the interface

### Implementing the actions

#### Implementing `createUser`

The `createUser` action is used by the  to request the creation of an  (a *requester*) in the .

> [!NOTE]
>
> If you opted to rename the action to something else than `createUser` in the "Configuring the interface" section, please use the new name instead of `createUser`.

Please refer to the expected input and output schemas for the action:
[interface.definition.ts line 85](https://github.com/botpress/botpress/blob/023d7de196e63f1a1b7cac0cf4f97c3d4aac940f/interfaces/hitl/interface.definition.ts#L85).

This action should implement the following logic:

**1. Create a Botpress user**

Create a Botpress user using the Botpress client by calling the `client.createUser()` method.

**2. Create an external user**

Create an  on the  using the 's API or SDK.

**3. Map the external user to the Botpress user**

Update the  on the  to map it to the Botpress user. Please refer to the 's documentation to know how to set extra metadata for the . The  must be able at any time to query the  in order to retrieve the Botpress user ID from the .

**4. Map the Botpress user to the external user**

Update the Botpress user to map it to the . This is typically done by setting a tag on the Botpress user with the 's ID.

**5. Yield control back to the plugin**

Yield control back to the plugin by returning an object containing the Botpress user's ID.

As reference, here's how this logic is implemented in the Zendesk integration:

```typescript src/index.ts {4,15,19,25,29,33,35} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new bp.Integration({
  actions: {
    async createUser({ ctx, input, client }) {
      // Create a Botpress user:
      const { name, email, pictureUrl } = input
      const { user } = await client.createUser({
        name,
        pictureUrl,
        tags: {
          email,
          role: 'end-user',
        },
      })

      // Create an external user on Zendesk:
      const zendeskClient = getZendeskClient(ctx.configuration)
      const zendeskUser = await zendeskClient.createOrUpdateUser({
        role: 'end-user',
        external_id: user.id, // <= map to the Botpress user ID
        name,
        email,
        remote_photo_url: pictureUrl,
      })

      // Map the Botpress user to the external user:
      await client.updateUser({
        id: user.id,
        tags: {
          id: zendeskUser.id.toString(), // <= map to the external user ID
        },
      })

      // Yield control back to the plugin and return the user ID:
      return {
        userId: user.id, // <= return the Botpress user ID
      }
    },
  },
})
```

#### Implementing `startHitl`

The `startHitl` action is used by the  to request the creation of a  (typically a *ticket*) in the .

> [!NOTE]
>
> If you opted to rename the action to something else than `startHitl` in the "Configuring the interface" section, please use the new name instead of `startHitl`.

Please refer to the expected input and output schemas for the action:
[interface.definition.ts line 109](https://github.com/botpress/botpress/blob/023d7de196e63f1a1b7cac0cf4f97c3d4aac940f/interfaces/hitl/interface.definition.ts#L109).

This action should implement the following logic:

**1. Fetch the Botpress user**

Fetch the Botpress user with ID `input.userId` that was passed in the input parameters.

**2. Retrieve the external user's ID**

From the Botpress user's tags, retrieve the 's ID.

**3. Create a Botpress conversation**

Create a Botpress conversation using the Botpress client by calling the `client.getOrCreateConversation()` method.

**4. Create the HITL session**

On the , create the . This is typically represented as a *ticket* in the .

**5. Map the Botpress conversation to the HITL session**

Update the Botpress conversation to map it to the . This is typically achieved by setting a `ticketId` tag on the Botpress conversation.

**6. Map the HITL session to the Botpress conversation**

Update the  on the  to map it to the Botpress conversation. Please refer to the 's documentation to know how to set extra metadata for the  (typically a *ticket*). The  must be able at any time to query the  in order to retrieve the Botpress conversation ID from the .

**7. Yield control back to the plugin**

Yield control back to the plugin by returning an object containing the Botpress conversation's ID.

As reference, here's how this logic is implemented in the Zendesk integration:

```typescript src/index.ts {4,9,17,24,27,31,35,37,40,42} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new bp.Integration({
  actions: {
    async startHitl({ ctx, input, client }) {
      // Fetch the Botpress user that was passed in the input parameters:
      const { user } = await client.getUser({
        id: input.userId,
      })

      // From the user's tags, retrieve the external user's id:
      const zendeskAuthorId = user.tags.id
      if (!zendeskAuthorId) {
        throw new sdk.RuntimeError(
          `User ${user.id} isn't linked to a Zendesk user`
        )
      }

      // Create a new ticket on Zendesk:
      const zendeskClient = getZendeskClient(ctx.configuration)

      const ticketTitle = input.title ?? 'Untitled Ticket'
      const ticketBody = 'A user created a support ticket'
      const createdZendeskTicket = await zendeskClient
        .createTicket(ticketTitle, ticketBody, {
          id: zendeskAuthorId, // <= map the ticket to the external user ID
        })

      // Create a Botpress conversation and map it to the Zendesk ticket:
      const { conversation } = await client.getOrCreateConversation({
        channel: 'hitl',
        tags: {
          id: createdZendeskTicket.id.toString(), // <= map to the ticket ID
        },
      })

      // Map the Zendesk ticket to the Botpress conversation:
      await zendeskClient.updateTicket(createdZendeskTicket.id, {
        external_id: conversation.id, // <= map to the Botpress conversation ID
      })

      // Yield control back to the plugin and return the conversation ID:
      return {
        conversationId: conversation.id, // <= return the Botpress conversation ID
      }
    },
  },
})
```

#### Relaying the conversation history

The input parameters of the `startHitl` action contain a `messageHistory` parameter. This parameter contains the conversation history that should be relayed to the  to provide the  with context about the conversation. This parameter is an array of every message that was sent in the conversation prior to the  being started.

**TypeScript schema for the messageHistory parameter**

```typescript theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
type MessageHistory = Array<
  | TextMessage
  | ImageMessage
  | AudioMessage
  | VideoMessage
  | FileMessage
  | LocationMessage
  | CarouselMessage
  | CardMessage
  | DropdownMessage
  | ChoiceMessage
  | BlocMessage
  | MarkdownMessage
>;

type Source =
  | {
      type: "user";
      userId: string;
    }
  | {
      type: "bot";
    };

interface TextMessage {
  source: Source;
  type: "text";
  payload: {
    text: string;
  };
}

interface ImageMessage {
  source: Source;
  type: "image";
  payload: {
    imageUrl: string;
  };
}

interface AudioMessage {
  source: Source;
  type: "audio";
  payload: {
    audioUrl: string;
  };
}

interface VideoMessage {
  source: Source;
  type: "video";
  payload: {
    videoUrl: string;
  };
}

interface FileMessage {
  source: Source;
  type: "file";
  payload: {
    fileUrl: string;
    title?: string;
  };
}

interface LocationMessage {
  source: Source;
  type: "location";
  payload: {
    latitude: number;
    longitude: number;
    address?: string;
    title?: string;
  };
}

interface CarouselMessage {
  source: Source;
  type: "carousel";
  payload: {
    items: Array<{
      title: string;
      subtitle?: string;
      imageUrl?: string;
      actions: Array<{
        action: "postback" | "url" | "say";
        label: string;
        value: string;
      }>;
    }>;
  };
}

interface CardMessage {
  source: Source;
  type: "card";
  payload: {
    title: string;
    subtitle?: string;
    imageUrl?: string;
    actions: Array<{
      action: "postback" | "url" | "say";
      label: string;
      value: string;
    }>;
  };
}

interface DropdownMessage {
  source: Source;
  type: "dropdown";
  payload: {
    text: string;
    options: Array<{
      label: string;
      value: string;
    }>;
  };
}

interface ChoiceMessage {
  source: Source;
  type: "choice";
  payload: {
    text: string;
    options: Array<{
      label: string;
      value: string;
    }>;
  };
}

interface BlocMessage {
  source: Source;
  type: "bloc";
  payload: {
    items: Array<
      | BlocTextItem
      | BlocMarkdownItem
      | BlocImageItem
      | BlocAudioItem
      | BlocVideoItem
      | BlocFileItem
      | BlocLocationItem
    >;
  };
}

interface BlocTextItem {
  type: "text";
  payload: {
    text: string;
  };
}

interface BlocMarkdownItem {
  type: "markdown";
  payload: {
    markdown: string;
  };
}

interface BlocImageItem {
  type: "image";
  payload: {
    imageUrl: string;
  };
}

interface BlocAudioItem {
  type: "audio";
  payload: {
    audioUrl: string;
  };
}

interface BlocVideoItem {
  type: "video";
  payload: {
    videoUrl: string;
  };
}

interface BlocFileItem {
  type: "file";
  payload: {
    fileUrl: string;
    title?: string;
  };
}

interface BlocLocationItem {
  type: "location";
  payload: {
    latitude: number;
    longitude: number;
    address?: string;
    title?: string;
  };
}

interface MarkdownMessage {
  source: Source;
  type: "markdown";
  payload: {
    markdown: string;
  };
}
```

If you decide to relay the conversation history to the , you can do so by iterating over the `messageHistory` array and sending each message to the  using its API or SDK. However, doing so might cause a significant number of notifications being sent to the . To alleviate this, you can choose to send only the last few messages in the conversation history, or to concatenate the messages into a single message. For example you could combine messages like this:

```markdown theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
## User1 said:
> Hello, I need help with my order.

## Bot replied:
> I have escalated this conversation to a human agent. Please wait while I connect you.
```

#### Adding extra parameters to the `startHitl` action

If the  requires extra parameters when starting a , you can add them to the `hitlSession` entity, or whichever name you chose for the entity in the [Adding the interface to your integration definition file](#adding-the-interface-to-your-integration-definition-file) section. For example, if the  requires a `priority` parameter, you can add it like this:

```typescript integration.definition.ts {5} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new sdk.IntegrationDefinition({
  entities: {
    ticket: {                     // <= name of your entity
      schema: sdk.z.object({
        priority: sdk.z.string().optional(), // <= add the extra parameter here
      })
    },
  },
})
```

Doing so will display the `priority` parameter in the "Start HITL" card within the Botpress Studio, allowing the bot authors to set it. The value of this parameter will then be passed to the `startHitl` action as part of the `hitlSession` input parameter:

```typescript src/index.ts {5,10} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new bp.Integration({
  actions: {
    async startHitl({ ctx, input, client }) {
      // Retrieve the extra parameter:
      const priority = input.hitlSession?.priority ?? 'normal'

      // Then use it to create the HITL session:
      const createdZendeskTicket = await zendeskClient
        .createTicket(ticketTitle, ticketBody, {
          priority, // <= pass the extra parameter to the external service
        })
    },
  },
})
```

#### Implementing `stopHitl`

The `stopHitl` action is used by the  to request the closure of a  (typically a *ticket*) in the .

> [!NOTE]
>
> If you opted to rename the action to something else than `stopHitl` in the "Configuring the interface" section, please use the new name instead of `stopHitl`.

Please refer to the expected input and output schemas for the action:
[interface.definition.ts line 162](https://github.com/botpress/botpress/blob/023d7de196e63f1a1b7cac0cf4f97c3d4aac940f/interfaces/hitl/interface.definition.ts#L162).

This action should implement the following logic:

**1. Fetch the Botpress conversation**

Fetch the Botpress conversation with ID `input.conversationId` that was passed in the input parameters.

**2. Retrieve the HITL session's ID**

From the Botpress conversation's tags, retrieve the 's ID.

**3. Close the HITL session**

On the , close the . This is typically involves *resolving* or *closing* a *ticket* in the .

**4. Yield control back to the plugin**

Yield control back to the plugin by returning an empty object.

> [!NOTE]
>
> The input parameters contain an unused `reason` parameter. Please ignore it. This parameter will be removed in future versions of the .

As reference, here's how this logic is implemented in the Zendesk integration:

```typescript src/index.ts {4,9,17,22} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new bp.Integration({
  actions: {
    async stopHitl({ ctx, input, client }) {
      // Fetch the Botpress conversation that was passed in the input parameters:
      const { conversation } = await client.getConversation({
        id: input.conversationId,
      })

      // From the conversation's tags, retrieve the Zendesk ticket's id:
      const ticketId: string | undefined = conversation.tags.id
      if (!ticketId) {
        return {}
      }

      const zendeskClient = getZendeskClient(ctx.configuration)

      // Close the ticket on Zendesk:
      await zendeskClient.updateTicket(ticketId, {
        status: 'closed',
      })

      // Yield control back to the plugin:
      return {}
    },
  },
})
```

### Implementing the channel

The `hitl` channel is used by the  relay  messages to the , which is usually a *ticket* or *thread* in the .

> [!NOTE]
>
> If you opted to rename the channel to something else than `hitl` in the "Configuring the interface" section, please use the new name instead of `hitl`.

This channel handler should implement the following logic:

**1. Retrieve the HITL session's ID**

From the Botpress conversation's tags, retrieve the 's ID.

**2. Retrieve the external user's ID**

* If the payload contains a `userId` parameter, the message has been sent by the . Retrieve the 's ID from the tags of the Botpress user `payload.userId`.
* If the payload doesn't contain a `userId` parameter, the message has been sent by the bot. Retrieve the 's ID from the tags of the attached Botpress user.

**3. Send the message to the HITL session**

Using the 's API or SDK, send the message to the . This is typically a *comment* in a *ticket*.

As reference, here's how this logic is implemented in the Zendesk integration:

```typescript src/index.ts {6,9,17} theme={"theme":{"light":"light-plus","dark":"dark-plus"}}
export default new bp.Integration({
  channels: {
    hitl: {
      messages: {
        async text({ client, conversation, ctx, payload, user }) {
          // Retrieve the ticket id from the conversation's tags:
          const zendeskTicketId = conversation.tags.id

          // Retrieve the external user:
          let zendeskAuthorId = user.tags.id

          if (payload.userId) {
            const { user: botpressUser } = await client.getUser({ id: payload.userId })
            zendeskAuthorId = botpressUser.tags.id
          }

          // Send the message to Zendesk:
          return await getZendeskClient(ctx.configuration)
            .createComment(zendeskTicketId, zendeskAuthorId, payload.text)
        },
      },
    },
  }
})
```

### Implementing the events

You should set up webhooks so that the  receives notifications about these events:

* A new message is added to the  (usually a *ticket*).
* A  has been assigned to the .
* The  was closed.

#### Incoming messages

When notified by the  that a new message has been added to the , you should relay the message to the Botpress conversation:

**1. Retrieve the Botpress conversation's ID**

Retrieve the Botpress conversation's ID from the 's metadata.

**2. Retrieve the external user's ID**

Retrieve the 's ID from the 's metadata.

**3. Add a message to the Botpress conversation**

Using the Botpress client, add a message to the Botpress conversation by calling the `client.createMessage()` method.

#### Implementing `hitlAssigned`

When notified by the  that a  has been assigned to the , you should notify the  by emitting the `hitlAssigned` event:

**1. Retrieve the Botpress conversation's ID**

Retrieve the Botpress conversation's ID from the 's metadata.

**2. Retrieve the external user's ID**

Retrieve the 's ID from the 's metadata.

**3. Emit the hitlAssigned event**

Using the Botpress client, emit the `hitlAssigned` event by calling the `client.createEvent()` method.

> [!NOTE]
>
> If you opted to rename the event to something else than `hitlAssigned` in the "Configuring the interface" section, please use the new name instead of `hitlAssigned`.

#### Implementing `hitlStopped`

When notified by the  that the  was closed, you should notify the  by emitting the `hitlStopped` event:

**1. Retrieve the Botpress conversation's ID**

Retrieve the Botpress conversation's ID from the 's metadata.

**2. Retrieve the external user's ID**

Retrieve the 's ID from the 's metadata.

**3. Emit the hitlStopped event**

Using the Botpress client, emit the `hitlStopped` event by calling the `client.createEvent()` method.

> [!NOTE]
>
> If you opted to rename the event to something else than `hitlStopped` in the "Configuring the interface" section, please use the new name instead of `hitlStopped`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.