Create Reusable Chatbot Templates For Your SaaS Business With Strapi
If you are a chatbot developer, selling the same service to multiple customers, you probably want to make as few changes as possible in your code for each new customer. If possible, you probably want to run the exact same code across all your customers, in order to make new deployments and maintenance as easy as possible. This is the basis of a scalable SaaS business: each new customer should cost only marginally more than the previous one, and adding new features or fixing bugs should benefit all customers at once.
In this article, I will show how to connect a custom CSML chatbot template with Strapi to make it very reusable, and allow you to create your own Chatbot as a Service business very easily!
The Target Customer
Let's say that your target customers are all your friendly neighbourhood hardware stores, to help them provide on-demand IKEA furniture assembly services via a simple chatbot. The goal of the chatbot is for the user to select what type of furniture they want to have assembled and receive a quote for the service.
The Chatbot
If you were to create a single-use chatbot for this use case (if you only had one single customer), the code would probably look like this:
start:
say "Hi, welcome to Tony Stark Hardware!"
say "We are open Mon-Fri, from 9AM to 6PM."
say "Our address is: 200 Park Avenue, New York, NY 10166, USA"
services:
say "Please select the type of furniture you would like us to assemble for you:"
say Carousel(cards=[
Card("Tables", subtitle="I need help with assembling my table", buttons=[Button("Tables") as btnTable]),
Card("Chairs", subtitle="I need help with assembling my chair", buttons=[Button("Chairs") as btnChair]),
Card("Desks", subtitle="I need help with assembling my desk", buttons=[Button("Desks") as btnDesk]),
])
hold
if (event.match(btnTable)) {
say "The cost for assembling a table is $25"
remember selected = "table"
goto end
}
else if (event.match(btnChair)) {
say "The cost for assembling a chair is $18"
remember selected = "chair"
goto end
}
else if (event.match(btnDesk)) {
say "The cost for assembling a desk is $34"
remember selected = "desk"
goto end
}
say "I did not understand!"
goto services
Obviously, this example is oversimplified, but it should help us make our point. Now what happens if you get a second customer asking for the same type of chatbot, except they don't have the same name and address (obviously) and provide slightly different services (i.e they don't assemble desks, but they do assemble beds) at different prices? You could easily just copy/paste the code and make the required modifications, and again for the next customer, and the next one so on. But there is a better, more scalable way to do so!
The Backend
As you may have noticed, all of the services and informations about the shop could easily be saved into variables, for example:
start:
do greetingsText = "Hi, welcome to Tony Stark Hardware!"
do openingHours = "We are open Mon-Fri, from 9AM to 6PM."
do address = "Our address is: 200 Park Avenue, New York, NY 10166, USA"
say greetingsText
say openingHours
say address
// etc. ...
What we would like to do is retrieve the value of the variables dynamically, based on the shop that this request is for. Ideally, we would like to have a backend that would be able to retrieve these values for us, something like:
do greetingsText = Fn("getGreetingsFromBackend")
Enter Strapi, a fantastic open-source "headless CMS", which is a fancy word for "a customisable API solution that helps you manage and serve dynamic content". Luckily, CSML Studio already has a Strapi app, so integrating Strapi in our chatbot will be extremely easy!
To get started easily, I will simply install Strapi on my own machine locally, but there are plenty of other deployment options. For now, a simple npx create-strapi-app my-project --quickstart
will nicely launch a ready-to-use Strapi instance, available on http://localhost:1337. Setup your admin user, and you're ready to go!
To manage your content, you will need to setup 2 different Content Types. One is for the products available at each of the shops, and evidently one is for each shop. Both must be collection types, as we will have multiple entries for each of them.
For the Products
type, we will add a name, a description, a coverImage and a price, which should be self explanatory.
For the Shops
type, we will need a few simple fields (name, openingHours, greetingText, address), but also two special fields: shopId, of UID type and a products field, a relation to the shop's products.
We will talk about the shopId
field later, for now you only need to know that the UID type guarantees that the value must be unique across all entries. While two separate shops could have the same name (i.e Apple Store
), they must have a very unique ID that sets them apart in the database.
The products
field is a relation field that tells the database which product belongs to which shop. When creating this field, make sure to use the "Shop belongs to many Products" relationship, as shown below:
Setting up the Content
Now that we are all set with our structure, let's create a few shops and products.
When creating the shops, remember to set a unique shopId value for each. It does not have to be a complicated value!
Here is an example for a sample shop called shopA, with shopId "aaa"
(because why not, but you are free to pick a more unique and recognizable value!).
When setting up each product, remember to select which shop it belongs to! If 2 shops have a product with the same name, you should create it twice, as perhaps they want to use a different description or picture, or the price may be different. In our case, both shops are providing chair assembling services:
Here is an example product. Note that the Shop
relationship panel on the right is set to shopA
.
Connecting the Chatbot With Strapi
Now that we have our content ready, we still have a few more steps to do. Strapi being secured by default, no content is accessible unless an authenticated and authorized user is performing the request. Let's create a user first. Navigate to the Users
collection type and create a new user with the Public
role.
Then, under Settings > Users and permissions > Roles, select the Public
role and make sure it can perform count
, find
and findone
operations on both Products and Shops collections:
If you are hosting Strapi on your own machine, you also need to make the instance available to the internet. An easy way to do so is by using ngrok (only in during development!): ngrok http 1337
will create a new endpoint for your Strapi instance:
And finally, you also need to install the Strapi app in your CSML chatbot, and setting the Strapi endpoint and the username and password of your Public user in the configuration:
We are all set for now. Let's see how we can change our code to make our bot truly reusable and scalable!
Adapting the Code
Taking our code from earlier, we want to replace every variable with a dynamic value fetched from our API. Let's start with the introduction:
start:
remember shop = Fn("strapi", endpoint="/shops?shopId=aaa")
// this should return an array of matching shops, which should be a single shop as we are requesting only shopId aaa
if (!shop.success || shop.data.length() != 1) {
say "Invalid shopId"
goto end
}
// simplify data access
remember shop = shop.data[0]
say shop.greetingText
say shop.openingHours
say "Our address is: {{shop.address}}"
goto services
Now, let's dynamically show the accepted products:
services:
say shop.description
do STRAPI_URL = YOUR_STRAPI_ENDPOINT // replace with your own value
do cards = []
do options = []
foreach (product) in shop.products {
do button = Button(
product.name,
productId=product.id,
price=product.price,
payload="PRODUCT:{{product.id}}",
)
do options.push(button)
do cards.push(Card(
product.name,
image_url="{{STRAPI_URL}}{{product.coverImage.formats.medium.url}}",
subtitle=product.description,
buttons=[button]
))
}
say Carousel(cards)
hold
do selected = event.match_array(options)
if (selected) {
say "The cost for {{selected.title}} is ${{selected.price}}"
remember productId = selected.productId
goto end
}
say "I did not understand!"
goto services
Let's explain what this code does. The best way to display products in a bot is by using a carousel. To generate the carousel, we need to iterate over the available products for each shop, which are part of the data returned by the call to the Strapi backend.
Each product will get its own card, and each card will have a single button allowing the user to select one of the options. The button also embeds some metadata for parsing it later on in the bot, for example the price and name of the item, or its ID.
Then, when a user clicks on a button, we match the value of the click with all the buttons and we make sure that it was one of the available options. Then we simply display the price and name of the product, and continue with our conversation. In this simple scenario, we just end the conversation there and then, but perhaps you would like to book a time for the intervention or get the user's phone number to give them a call later on. It's up to you!
Here is the end result of our code:
Onboarding New Customers
Let's say that you have a new customer for your chatbot. Yay! Now it's just a case of adding a new shop in the Strapi backend, and giving it a unique ID and giving them a way to include the chatbot on their website so that their visitors can access their services through the chatbot.
So let's do this!
In CSML Studio, visit the Channels section, and create a new webapp channel, give it a suitable name and description for your customer. Now, scroll down to the bottom of that window and grab the chatbox script:
As shown in the documentation, you can easily add custom metadata to this script. For example, a shopId
value! This will be injected into your conversation everytime a user visits the chatbot. You need to URL-encode the JSON value first (you can use this tool to do so).
input: {"shopId":"aaa"}
output: %7B%22shopId%22%3A%22aaa%22%7D
Your script should then look like this:
<script
src="{CHATBOX_URL}"
id="clevy-chatbox"
data-webapp-metadata="%7B%22shopId%22%3A%22aaa%22%7D"
async></script>
Now, when a user comes to use the chatbot, the _metadata.shopId
value is available and should be set to "aaa"
.
Let's change our query to Strapi accordingly:
// perhaps add some error handling in case the script is misconfigured:
if (!_metadata.shopId) {
say "Missing shopId!"
goto end
}
// dynamically retrieve the shopId value from the metadata
remember shop = Fn("strapi", endpoint="/shops?shopId={{_metadata.shopId}}")
And voilà! You are definitely all set. Now your chatbot is fully dynamic and can react in real-time to changes in your database. You can add new customers or remove existing ones without ever touching the chatbot code, and if you need to change something in the chatbot, you don't need to replicate your changes across all your customers as they will all be updated instantly.
Conclusion
Obviously, this is just an oversimplified example. But it showcases an important pattern in development: how to refactor your code to make it more maintainable, scalable, and decoupled from your business needs so that you can grow your business without having to change your code too much.
Strapi is a great solution to very quickly create powerful, customisable application backends. And it works very well with chatbots too!