Using Terraform to Templatize REST API calls to Dialogflow
Overview
Dialogflow is a powerful tool in Google Cloud that you can use to design conversational agents powered by Natural Language Understanding (NLU) to transform user requests into actionable data. You can integrate voice and/or chat agents in your app, website, or customer support systems to determine user intent and interact with users.

Problem: When managing Dialogflow agents with Terraform (or other things with Terraform), sometimes the Terraform module that you are working with doesn't have an argument for the specific setting you need, but the REST API does. If you were to make ad-hoc REST API calls (i.e., outside of Terraform), those configuration settings would not be managed the same way as all of your other infrastructure as code! 😭
Solution: Construct the REST API call that you want to use, then templatize and manage your REST API call within your Terraform configuration files. Now you can manage the custom settings that you need, and still get all of the benefits of Terraform and infrastructure as code principles. 🏆
Sample Terraform code
In this post, we'll use the
Terraform modules for Dialogflow CX
and walk through the steps to develop and templatize a REST API call in
Terraform using the local-exec
provisioner along with a null_resource
. You
can grab the
Terraform + Dialogflow scripts
and use them in your own Google Cloud account.
Try out the Terraform + Dialogflow scripts
Let's templatize some REST API calls in Terraform and cover some best practices for Dialogflow and Terraform along the way!
Setup
There are a few things that you'll need to set up before you can work with Dialogflow and Terraform to follow the steps in this post:
- Register for a Google Cloud account.
- Enable the Dialogflow API.
- Install and initialize the
Google Cloud
gcloud
CLI tool. - Create a service account and associated JSON key per the Terraform documentation and Google Cloud documentation.
- Finally, install
Terraform. If you're
using macOS, I recommend installing Terraform with
Homebrew. Or, even better, install
tfenv using
brew install tfenv
, then runtfenv install
so you can easily switch between versions of Terraform!
Initial Terraform setup
We'll start by creating two Terraform configuration files, one for the cloud provider configuration, and one with variables that we'll throughout the configuration files.
First, in a new directory, create a file called variables.tf
with the
following contents:
variable "project_id" {
default = "your-project-id"
type = string
}
variable "region" {
default = "us-central1"
type = string
}
variable "zone" {
default = "us-central1-a"
type = string
}
where project_id
is your Google Cloud project ID, and region
and zone
refer to your desired Google Cloud region and zone, respectively. Be sure to
replace the placeholder values on the default = ""
lines with your preferred
values.
Next, create a file called provider.tf
with the following contents:
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}
You should now have a folder with two .tf
files as in:
terraform/
├── provider.tf
└── variables.tf
From within the directory that contains your new Terraform configuration files, you can run the following command to initialize Terraform and install the required plugins:
terraform init
If successful, you'll see output similar to the following after Terraform installs the Google Cloud provider plugin:
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v4.48.0...
- Installed hashicorp/google v4.48.0 (signed by HashiCorp)
[...]
Terraform has been successfully initialized!
Create a new Dialogflow agent
Next, we'll use Terraform to create a (very) simple agent in Dialogflow CX.
In the same directory as your other .tf
files, create a file called
agents.tf
with the following contents:
resource "google_dialogflow_cx_agent" "agent" {
display_name = "Greeting Bot"
location = var.region
default_language_code = "en"
time_zone = "America/Chicago"
}
You can also change the default_language_code
and time_zone
if you want.
Now, you can run the following command to apply the Terraform configuration:
terraform apply
You should then see output similar to the following after Terraform determines the plan for which resources to create:
Terraform will perform the following actions:
# google_dialogflow_cx_agent.agent will be created
+ resource "google_dialogflow_cx_agent" "agent" {
+ default_language_code = "en"
+ display_name = "Greeting Bot"
+ id = (known after apply)
+ location = "us-central1"
+ name = (known after apply)
+ project = (known after apply)
+ start_flow = (known after apply)
+ time_zone = "America/Chicago"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Enter yes
to confirm the plan, then Terraform will create your Dialogflow
agent:
google_dialogflow_cx_agent.agent: Creating...
google_dialogflow_cx_agent.agent: Creation complete after 2s [id=projects/your-project-id/locations/us-central1/agents/cfc938fc-c616-45f3-807e-2b7d68bc815d]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Switch to the
Dialogflow Console in your
browser and the project where you created your agent, where you can admire your
newly created Greeting Bot
agent.

Edit default welcome intent
When you create an agent, Dialogflow automatically creates a
default welcome intent
for you. Let's take a closer look at the default welcome intent, which you can
find by navigating to Manage
> Intents
> Default Welcome Intent
in the
Dialogflow Console:

Now, let's assume that we want to modify the default welcome intent to use different training phrases instead of the default training phrases:

Instead of using the default training phrases just going to say hi
, heya
,
hello hi
, howdy
, hey there
, hi there
, greetings
, hey
,
long time no see
, and hello
, we want to use the new training phrases
hi how are you
, greetings
, howdy
, hello
, hello there
, hi
,
nice to meet you
, whats up
, hows it going
, hey
.
We could modify the training phrases directly in the Dialogflow CX console, save the intent, and move on. But then we lose out on the benefits of an infrastructure as code approach! What about the reproducibility, change management, or version control of this modification?
Let's explore how to make that change with Terraform instead of doing it manually.
Terraform to the rescue, maybe
Can we use Terraform directly to modify the intent?
Easy enough, right? We'll just use the
google_dialogflow_cx_intent
module in Terraform
to make the change, and move on to other things.
Let's create a new Terraform configuration file called intents.tf
with the
following contents:
resource "google_dialogflow_cx_intent" "default_welcome_intent" {
parent = google_dialogflow_cx_agent.agent.id
display_name = "Default Welcome Intent"
training_phrases {
repeat_count = 1
parts {
text = "hi how are you"
}
}
}
Then we can apply the configuration by using terraform apply
and confirm, and
the result is:
google_dialogflow_cx_intent.default_welcome_intent: Creating...
╷
│ Error: Error creating Intent: googleapi: Error 409: com.google.apps.framework.request.AlreadyExistsException: Intent with display name 'Default Welcome Intent' already exists in the agent. Code: ALREADY_EXISTS
│
│ with google_dialogflow_cx_intent.default_welcome_intent,
│ on intents.tf line 1, in resource "google_dialogflow_cx_intent" "default_welcome_intent":
│ 1: resource "google_dialogflow_cx_intent" "default_welcome_intent" {
Fighting with existing resources
What?! An error creating the intent. Oh no!
Remember that when we created the agent with Terraform, Dialogflow also created
a default welcome intent, as it does with all new agents. So when we try to
create an intent called Default Welcome Intent
, it fails with an API error
since a resource like that already exists, and Terraform isn't explicitly aware
of the default welcome intent.
Given this behavior, how can we use Terraform to make a change if the requested resource already exists? How can we get beyond this conflict?
Can we import the resource into Terraform?
Ok, new plan. What if we import the resource into Terraform, modify the settings that we want, then apply the changes?
We could, but then we would need to apply an initial set of Terraform configuration files to create an agent, then import the default welcome intent, then re-apply the Terraform state, resolve conflicts and dependencies, re-apply, and so on. And the complexity of this approach would grow as we make additional customizations to other intents, flows, and other default components in Dialogflow.
While this is technically feasible, it's not the cleanest solution, and it's not the best pattern to use. We need another way of modifying the default welcome intent that works well with Terraform.
There must be a better way!
Since we can't modify the default welcome intent with Terraform, we could make a REST API call to modify the training phrases. 🤔 The REST API request would be represented as code, which means that we can also manage it with Terraform. 💡
In the following sections, we'll walk through the steps to make that happen!
Get information about an intent
Before we get into the details of Terraform and parameterizing REST API calls, let's first construct a valid REST API call that gets information about the default welcome intent. Otherwise we'll be trying to get too many things to work together at the same time, which will make for some complicated debugging later. Let's start simple instead!
Referring to the
REST API documentation for Dialogflow CX,
we see that there is a method to
get information about an intent in Dialogflow.
We can use this information to construct a REST API request with curl
that
uses your Google Cloud project ID, location, agent ID, and the
default welcome intent ID,
which you can then run in your terminal:
export PROJECT=your-project-id
export LOCATION=us-central1
export AGENT=5c6112d8-85bc-4432-b67e-c8d7090cc5c1
export INTENT=00000000-0000-0000-0000-000000000000
curl --location --request GET "https://$LOCATION-dialogflow.googleapis.com/v3/projects/$PROJECT/locations/$LOCATION/agents/$AGENT/intents/$INTENT" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)"
When you execute this REST API request in your terminal, you should see output with detailed information about the default welcome intent, including the same default training phrases that we saw in the console:
{
"name": "projects/your-project-id/locations/us-central1/agents/5c6112d8-85bc-4432-b67e-c8d7090cc5c1/intents/00000000-0000-0000-0000-000000000000",
"displayName": "default welcome intent",
"trainingPhrases": [
{
"id": "19cd2285-4807-4ad5-b313-26f3191310e7",
"parts": [
{
"text": "just going to say hi"
}
],
"repeatCount": 1
},
{
"id": "59f99b0b-054e-415b-802b-f5901fcaf8c3",
"parts": [
{
"text": "heya"
}
],
"repeatCount": 1
},
[...]
}
Perfect! The default welcome intent object that we inspected contains exactly the training phrases that we want to modify. Now, rather than just asking for details about the intent, let's change the training phrases in the default welcome intent using a different REST API request.
In the following sections, we'll walk through a good, better, and best approach so that we can clearly see the benefits in each step along the way without having too many moving parts.
Good: Modify an intent manually
Let's construct a REST API call that makes the desired changes to the default
welcome intent. Referring to the
REST API documentation for Dialogflow CX,
we see that there is a method to
PATCH
an intent in Dialogflow,
which will modify the intent in-place.
We can use this information to construct a REST API request with curl
that
uses your Google Cloud project ID, location, agent ID, and the default welcome
intent ID to modify the default welcome intent, which you can then run in your
terminal:
export PROJECT=your-project-id
export LOCATION=us-central1
export AGENT=5c6112d8-85bc-4432-b67e-c8d7090cc5c1
export INTENT=00000000-0000-0000-0000-000000000000
curl --location --request PATCH "https://$LOCATION-dialogflow.googleapis.com/v3/projects/$PROJECT/locations/$LOCATION/agents/$AGENT/intents/$INTENT?updateMask=trainingPhrases" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H 'Content-Type: application/json' \
--data-raw "
{
'trainingPhrases': [
{'parts': [{'text': 'hi how are you'}], 'repeatCount': 1},
{'parts': [{'text': 'greetings'}], 'repeatCount': 1},
{'parts': [{'text': 'howdy'}], 'repeatCount': 1},
{'parts': [{'text': 'hello'}], 'repeatCount': 1},
{'parts': [{'text': 'hello there'}], 'repeatCount': 1},
{'parts': [{'text': 'hi'}], 'repeatCount': 1},
{'parts': [{'text': 'nice to meet you'}], 'repeatCount': 1},
{'parts': [{'text': 'whats up'}], 'repeatCount': 1},
{'parts': [{'text': 'hows it going'}], 'repeatCount': 1},
{'parts': [{'text': 'hey'}], 'repeatCount': 1}
]
}
"
When you execute this REST API request in your terminal, you should see a response with the newly configured information in the default welcome intent, including the new training phrases:
{
"name": "projects/your-project-id/locations/us-central1/agents/5c6112d8-85bc-4432-b67e-c8d7090cc5c1/intents/00000000-0000-0000-0000-000000000000",
"displayName": "default welcome intent",
"trainingPhrases": [
{
"parts": [
{
"text": "hi how are you"
}
],
"repeatCount": 1
},
{
"parts": [
{
"text": "greetings"
}
],
"repeatCount": 1
},
[...]
}
Success! The new default welcome intent contains the new training phrases that we configured. Now, let's move on to how to templatize this REST API request and manage it with Terraform.
Better: Command in Terraform
A good approach is to save the REST API call from the previous section in a Bash
script, version control it, and remember to run it after we provision our
Dialogflow agent with Terraform. A better approach is to invoke the REST API
call from Terraform so that it applies our customizations all in the same,
single terraform apply
command!
Terraform has provisioners for cloud platforms such as Google Cloud. In addition
to provisioners for cloud platforms, it also has other provisioners such as the
local-exec
provisioner,
which can be used to run shell commands on the machine that we are running
Terraform from.
Terraform also has various resources, such as the modules that we are using for
Dialogflow CX. A special type of resource in Terraform is called the
null_resource
,
which can be used for arbitrary values/actions. This is perfect for us to use in
conjunction with the local-exec
provisioner to make our REST API call.
To accomplish this, you can put the following contents in a file called
intents.tf
, being sure to overwrite the (intentional) failed attempt that we
tried earlier:
resource "null_resource" "default_welcome_intent" {
provisioner "local-exec" {
command = <<-EOT
export PROJECT=your-project-id
export LOCATION=us-central1
export AGENT=5c6112d8-85bc-4432-b67e-c8d7090cc5c1
export INTENT=00000000-0000-0000-0000-000000000000
curl --location --request PATCH "https://$LOCATION-dialogflow.googleapis.com/v3/projects/$PROJECT/locations/$LOCATION/agents/$AGENT/intents/$DEFAULT_WELCOME_INTENT?updateMask=trainingPhrases" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H 'Content-Type: application/json' \
--data-raw "
{
'trainingPhrases': [
{'parts': [{'text': 'hi how are you'}], 'repeatCount': 1},
{'parts': [{'text': 'greetings'}], 'repeatCount': 1},
{'parts': [{'text': 'howdy'}], 'repeatCount': 1},
{'parts': [{'text': 'hello'}], 'repeatCount': 1},
{'parts': [{'text': 'hello there'}], 'repeatCount': 1},
{'parts': [{'text': 'hi'}], 'repeatCount': 1},
{'parts': [{'text': 'nice to meet you'}], 'repeatCount': 1},
{'parts': [{'text': 'whats up'}], 'repeatCount': 1},
{'parts': [{'text': 'hows it going'}], 'repeatCount': 1},
{'parts': [{'text': 'hey'}], 'repeatCount': 1}
]
}
"
EOT
}
All we've done in this step is moved our REST API curl
command (the command
that modifies the default welcome intent) from the previous section into a
Terraform configuration file.
Specifically, we now have the REST API command wrapped with a local-exec
provisioner and contained within a null_resource
. This means that Terraform
will treat this as a resource of its own and run the curl
command when we run
terraform apply
.
Best: Terraform with variables
The approach in the previous section is a decent solution for what we need to accomplish. However, what if you want to use a different Google Cloud project. Or what if you want to use a different agent? Or, can we populate the dynamic variables such as the agent ID using other information that Terraform knows about? You could manually change the values in the Terraform configuration files, but we can't have that kind of manual work in our best solution!
To improve upon the approach used in the previous section, let's convert the
static values into templated values using Terraform. Instead of using
export VARIABLE=value
directly in the local-exec
command, let's remove those
export
commands and instead use variables, templating, and interpolation,
which are a few of the capabilities that make Terraform awesome to work with in
situations like this.
To accomplish this, you can put the following contents in a file called
intents.tf
, being sure to overwrite all of the sample code from earlier
attempts:
resource "null_resource" "default_welcome_intent" {
provisioner "local-exec" {
command = <<-EOT
curl --location --request PATCH "https://$LOCATION-dialogflow.googleapis.com/v3/projects/$PROJECT/locations/$LOCATION/agents/$AGENT/intents/$DEFAULT_WELCOME_INTENT?updateMask=trainingPhrases" \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-H 'Content-Type: application/json' \
--data-raw "
{
'trainingPhrases': [
{'parts': [{'text': 'hi how are you'}], 'repeatCount': 1},
{'parts': [{'text': 'greetings'}], 'repeatCount': 1},
{'parts': [{'text': 'howdy'}], 'repeatCount': 1},
{'parts': [{'text': 'hello'}], 'repeatCount': 1},
{'parts': [{'text': 'hello there'}], 'repeatCount': 1},
{'parts': [{'text': 'hi'}], 'repeatCount': 1},
{'parts': [{'text': 'nice to meet you'}], 'repeatCount': 1},
{'parts': [{'text': 'whats up'}], 'repeatCount': 1},
{'parts': [{'text': 'hows it going'}], 'repeatCount': 1},
{'parts': [{'text': 'hey'}], 'repeatCount': 1}
]
}
"
EOT
environment = {
PROJECT = var.project_id
LOCATION = var.region
AGENT = google_dialogflow_cx_agent.agent.name
DEFAULT_WELCOME_INTENT = "00000000-0000-0000-0000-000000000000"
}
}
depends_on = [
google_dialogflow_cx_agent.agent
]
}
Great! Now we have templatized the REST API request in Terraform. In other words, we converted the static values into dynamic/templated values. This means that we shouldn't have to modify this Terraform configuration file in the future to change details such as the Google Cloud project ID, location, or the agent ID.
We could also configure the list of training phrases as a variable and render
the JSON in the API request payload using
for
expressions in Terraform,
but this blog post has to conclude at some point! 😅
Now let's run our new Terraform configuration! We can re-run the following
command to initialize Terraform and install an additional plugin (since we added
the null_resource
provisioner):
terraform init
And we can re-run the following command to apply the Terraform configuration again:
terraform apply
You should then see output similar to the following after Terraform determines the plan for which resources to create (or you might also see Terraform attempting to create the agent if you destroyed or deleted previous versions of your agent, hooray for destructive infrastructure! 💥):
Terraform will perform the following actions:
# null_resource.default_welcome_intent will be created
+ resource "null_resource" "default_welcome_intent" {
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Enter yes
to confirm the plan, then Terraform will make the REST API call to
change the training phrases:
null_resource.default_welcome_intent: Creating...
null_resource.default_welcome_intent: Provisioning with 'local-exec'...
null_resource.default_welcome_intent (local-exec): Executing: ["/bin/sh" "-c" "curl --location --request PATCH \"https://$LOCATION-dialogflow.googleapis.com/v3/projects/$PROJECT/locations/$LOCATION/agents/$AGENT/intents/$DEFAULT_WELCOME_INTENT?updateMask=trainingPhrases\" \\\n -H \"Authorization: Bearer $(gcloud auth application-default print-access-token)\" \\\n -H 'Content-Type: application/json' \\\n --data-raw \"\n {\n 'trainingPhrases': [\n {'parts': [{'text': 'hi how are you'}], 'repeatCount': 1},\n {'parts': [{'text': 'greetings'}], 'repeatCount': 1},\n {'parts': [{'text': 'howdy'}], 'repeatCount': 1},\n {'parts': [{'text': 'hello'}], 'repeatCount': 1},\n {'parts': [{'text': 'hello there'}], 'repeatCount': 1},\n {'parts': [{'text': 'hi'}], 'repeatCount': 1},\n {'parts': [{'text': 'nice to meet you'}], 'repeatCount': 1},\n {'parts': [{'text': 'whats up'}], 'repeatCount': 1},\n {'parts': [{'text': 'hows it going'}], 'repeatCount': 1},\n {'parts': [{'text': 'hey'}], 'repeatCount': 1}\n ]\n }\n \"\n"]
[...]
null_resource.default_welcome_intent: Creation complete after 1s [id=9172766381648641447]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Success! Our new training phrases are now configured in the default welcome intent, and we configured them all from a managed and templated REST API call with Terraform!

You can navigate to the simulator in the Dialogflow console to have a sample
conversation with Greeting Bot
and try things out interactively:

There's a lot of things going on in the final Terraform code sample, so let's review the key points and best practices that we implemented in this step.
Handling multi-line commands
What's that EOT
doing in the code for our command?! That's a heredoc
style
that we can use in the shell command to break our large command up across
multiple lines and make it easier to read and maintain. Your future self will
thank you for using heredoc
directives. You can learn more about using
heredoc
style strings in the Terraform
documentation.
PATCHing with update masks
You might have noticed a URL parameter in our PATCH request called
updateMask=trainingPhrases
. When updating Dialogflow agent data via the API,
you can choose to overwrite only specific fields to avoid accidentally
overwriting all of the other data contained within the intent, agent, or other
component. This also reduces the number of settings we have to repeat or
duplicate in our API request payload. You can learn more about updating data
with field masks in the Dialogflow
documentation.
Using environment variables
Rather than defining the environment variables directly in the templated REST
API curl
command, we moved the environment variables to the environment
block and defined them there. This makes it easier to manage and edit the
environment variables in the future using Terraform's interpolation and
templating functionality. You can learn more about using environment variables
in the Terraform documentation for the local-exec
provisioner.
Input variables in Terraform
Some of the environment variables that we've defined such as the PROJECT
and
LOCATION
are referring to Terraform variables that we've defined elsewhere. In
this case, we defined the input variables in a separate file called
variables.tf
in the
earlier section on Terraform setup. Now, if we ever
need to change the project or region, we can change it in one location without
having to search and replace all of our Terraform configuration files. You can
learn more about using input variables in the Terraform
documentation.
Referencing Terraform attributes
Instead of using a static value for the agent ID variable, we changed the
reference to refer to the name
attribute of the Terraform-managed Dialogflow
agent resource (i.e., the one that we defined in agents.tf
) by adding the
following environment variable definition within the local-exec
provisioner:
environment = {
AGENT = google_dialogflow_cx_agent.agent.name
}
This is also a helpful way to have Terraform use the full identifiers for
Dialogflow resources such as intents or pages (e.g.,
projects/{project}/locations/{location}/agents/{name}
), which is very
convenient when you are configuring settings such as transition routes in flows.
You can learn more about using attributes from resources in the Terraform
documentation for the Dialogflow CX
module.
Handling dependencies
You might also have noticed that we used a depends_on
block in the
null_resource
because we want to ensure that the REST API call only runs after
the agent is created. Otherwise the REST API call might fail (or even worse,
have non-deterministic behavior! ☠️) if it is run before the Dialogflow agent
exists.
In general, the fact that we referenced an attribute
(google_dialogflow_cx_agent.agent.name
) from the Dialogflow agent in the
null_resource
means that Terraform will do the right thing and handle the
dependency of resources properly. In other words, Terraform knows that it cannot
determine the name of the agent before it exists. However, it's always good to
be explicit rather than implicit when possible, especially since
null_resource
s and local-exec
s can be used for arbitrary tasks! You can
learn more about using handling hidden resources in the Terraform
documentation.
Congratulations, you made it all the way through to the best solution and learned about all of the best practices that we followed along the way! 🎉
Handling more complex cases
In this example, I tried to keep things simple(r) and focused on the methodology to work with REST API calls in Terraform by using a very simple Dialogflow agent with minimal configuration.
However, in practice, when you use REST API calls to modify custom settings for more complex Dialogflow agents, subsequent Terraform runs will often fail when you try to change, manage, or destroy resources and components since you've changed these settings outside of Terraform via a REST API call, which can have a cascading effect on other components.

To this end, if you want to explore a slightly more complex example that
modifies transition routes in the default start flow and requires the use of a
destroy-time provisioner and triggers in Terraform, refer to the previous post
on
Managing Dialogflow Agents with Terraform
and the accompanying Terraform + Dialogflow code sample on
GitHub. In particular,
you can examine the
terraform/flows.tf
file there since you know more about managing REST API calls with Terraform
after reading this blog post. 😁
Summary
In this post, we walked through the steps to construct and templatize a REST API
call to Dialogflow CX by using the local-exec
provisioner, null_resource
,
and environment variables in Terraform. In this example, we modified the default
welcome intent to use different training phrases instead of the default training
phrases.
You can also use this same approach to manage all of your custom Dialogflow agent settings that are available in the Dialogflow CX API while retaining all of the benefits of Terraform and infrastructure as code principles.
Once you represent your agent configuration and REST API calls in Terraform, then you can manage all of your agents and their settings programmatically, version control and track the state of your agents, spin up agents in dev/QA/prod environments for testing, and all of the other neat functionality that you get by using Terraform.
If you haven't already, now is a good time to get the Terraform + Dialogflow scripts and try them in your own Google Cloud account!
Try out the Terraform + Dialogflow scripts
Happy templatizing!