Build an IT Service Desk Bot
Build a Lex bot that creates and stores IT support tickets with Lambda.
Introduction
30 Second Summary
When a laptop loses its connection, the hardest part can be turning the problem into a clear request that reaches the right person. Missing details create extra messages before anyone can begin fixing it.
In this project, you will build ITHelpDeskBot, a text-based service desk assistant using Amazon Lex V2. Each completed chat becomes a referenced ticket through AWS Lambda and Amazon DynamoDB.
What You'll Build
You'll watch a laptop network issue become a referenced ticket that you can trace from the final chat response through its stored record into the fulfillment log.
By the end of this project, you'll have:
- A guided incident conversation that gathers the device type, issue category, description, plus priority through four short questions.
- A persisted ticket workflow that turns the completed report into a stored record with a visible reference such as INC-demo-001.
- A traceable evidence trail that lets you match one reference across the chat, Amazon DynamoDB, plus Amazon CloudWatch Logs.
- Secret Mission: Add a ticket status lookup that retrieves the stored status for an existing ticket reference.
Are there any prerequisites?
You'll need prior Amazon Lex and AWS Lambda experience plus an AWS account that can create each project resource. No local development tools are required because AWS CloudShell provides the browser-based build environment.
Before We Start
Consistent automated IT intake gives support teams the details they need before troubleshooting starts. This checkpoint commits you to demonstrating a laptop network report that returns a stored ticket reference through Amazon Lex, within a strict allowance of no more than 25 text inputs.
Create the Bot and Check the Environment
Your bot and its backend must live in one AWS Region. A split across Regions can block the integration later.
First, you will create a blank Amazon Lex V2 bot through the AWS Management Console.
Then, you will verify the tools in AWS CloudShell. This browser-based environment keeps your Mac free from extra installations.
In this step, get ready to:
- Choose one AWS Region for every project resource.
- Create the blank ITHelpDeskBot bot with its text-only language.
- Verify the CloudShell build tools.
Choose one AWS Region
AWS services are separated by Region. Choosing one Region now gives every resource in this project the same location.
- Open the AWS Management Console in your browser.
- Sign in with your AWS account.
- Use the Region menu in the top navigation bar to select one Region for this project.
- Record the Region you selected: your selected AWS Region.
- Keep a tally of every Amazon Lex text input during this project.
- Stop testing when your tally reaches the allowance of no more than 25 text inputs.
Why count every text input?
Each user text input sent to Amazon Lex is a separate billable request. Your testing allowance keeps the guided work predictable.
Create the blank Lex bot
A blank bot gives you control over the conversation you build in the next step. Lex can also create a basic service role for the bot during setup.
- Enter Amazon Lex in the console search bar.
- Select Amazon Lex from the search results.
- Choose Create bot.
- Select Traditional in the creation method section.
- Select Create a blank bot.
- Enter ITHelpDeskBot as the bot name.
- Choose the IAM permissions option that lets Amazon Lex create a role.
What does the basic role do?
The Lex-created role uses AWS Identity and Access Management permissions to let the bot access supporting AWS services. The fulfillment function you build later receives its own execution role.
- Choose the appropriate response in the Children's Online Privacy Protection Act (COPPA) section.
- Choose Next to create the bot.
- Choose English (US) in the language details.
- Choose None in the voice section.
- Keep the default classification confidence score threshold.
- Choose Add to add the language.
- Return to the bot overview.
You should see ITHelpDeskBot with English (US) and locale en_US. That is the bot shell ready for its first conversation.
Open CloudShell and verify the tools
The fulfillment package later depends on Node.js version 20 or newer. CloudShell also supplies npm, the AWS CLI, and the zip utility.
- Confirm that the console header still shows your selected AWS Region.
- Choose the CloudShell icon in the console navigation bar.
- Wait for the command prompt to confirm that the shell is ready.
Before you run the checks, do you expect every pre-installed tool to meet this project's requirements?
- Check the four tool versions by running these commands:
node --version
npm --version
aws --version
zip --version
What do these checks prove?
- The node result confirms whether the runtime can use the pinned DynamoDB client later.
- The npm result confirms that CloudShell can install the declared project dependency.
- The aws result confirms that the AWS command-line tools are available.
- The zip result confirms that CloudShell can create the Lambda deployment archive.
✔️ I see Node.js 20 or higher
The first line reports Node.js version 20 or newer. The remaining lines show version details for npm, the AWS CLI, and zip.
- Keep CloudShell open for the later packaging work.
ⓧ I see an older Node.js version
The pinned @aws-sdk/client-dynamodb version 3.1138.0 requires Node.js 20.0.0 or newer. Leave that dependency uninstalled until the runtime meets this requirement.
- Install Node Version Manager and activate the current long-term support release by running these commands:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
. ~/.bashrc
nvm install --lts && nvm alias default lts/*
What does this installation do?
The first command installs Node Version Manager in your shell environment. Reloading .bashrc makes the new command available.
The nvm command installs the current long-term support release. It also makes that release the default for later CloudShell sessions.
Before you check again, do you expect the Node.js major version to meet the minimum now?
- Verify all four tools again by running:
node --version
npm --version
aws --version
zip --version
What should you see now?
The Node.js output should report version 20 or newer. The other three commands should each print version information.
Node.js still below version 20?
Reload the shell before repeating the version checks. A failed download can also leave the previous runtime active.
Do not install the pinned DynamoDB client while the older runtime is active.
Help me activate Node.js 20 or newer in AWS CloudShell.
ⓧ node is not found
CloudShell normally includes Node.js. Installing a user-managed long-term support release gives this shell the required runtime.
- Install Node Version Manager and activate the current long-term support release by running these commands:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
. ~/.bashrc
nvm install --lts && nvm alias default lts/*
What does this installation do?
Node Version Manager installs Node.js inside your shell environment. The default alias keeps the selected long-term support release active when you return to CloudShell.
Before you verify the installation, do you expect the missing command to return a version now?
- Verify all four tools by running:
node --version
npm --version
aws --version
zip --version
What should you see now?
The Node.js output should report version 20 or newer. The remaining commands should each print their installed version.
Node.js still unavailable?
Confirm that the installation finished without a network error. Reload the shell before repeating the version checks.
Help me restore Node.js in AWS CloudShell.
Missing another tool version?
CloudShell includes npm, the AWS CLI, and zip. Confirm that the command prompt is ready before repeating the affected version check.
Help me troubleshoot a missing CloudShell tool.
You should now have ITHelpDeskBot open in Amazon Lex V2. CloudShell should report Node.js version 20 or newer plus versions for npm, the AWS CLI, and zip.
Your blank bot and browser-based build environment are ready. Next, you will give the bot a structured support conversation and see where a conversation alone falls short.
Build the Ticket Conversation
Your blank ITHelpDeskBot is ready in the selected Region. Your next goal is to turn it into a structured service desk conversation with Amazon Lex V2.
A useful intake conversation must gather the same details from every user. This test also reveals whether conversation capture alone can produce a traceable ticket.
In this step, get ready to:
- Create three restricted custom slot types.
- Configure the ReportIssue intent for the four-part conversation.
- Test the built bot to expose the missing backend.
Create restricted custom slot types
Custom slot types define the values Lex can recognize for a field. Restricted values keep your demonstration predictable.
- Switch back to ITHelpDeskBot in the Amazon Lex V2 console.
- Select the English (US) language.
- Select Slot types from the language menu.
The slot type list is where you create the controlled vocabulary for the conversation.
- Choose Add slot type.
- Choose Add blank slot type.
- Enter DeviceTypeSlotType in the Slot type name field.
- Add Laptop as the first slot value.
- Add Desktop as the second slot value.
- Add Mobile phone as the third slot value.
- Select Restrict to slot values.
- Choose Save slot type.
The first slot type now limits device answers to the three supported demo values.
- Choose Add slot type.
- Choose Add blank slot type.
- Enter IssueCategorySlotType in the Slot type name field.
- Add Password as the first slot value.
- Add Network as the second slot value.
- Add Software as the third slot value.
- Add Hardware as the fourth slot value.
- Select Restrict to slot values.
- Choose Save slot type.
The second slot type gives each issue one consistent category.
- Choose Add slot type.
- Choose Add blank slot type.
- Enter PrioritySlotType in the Slot type name field.
- Add Low as the first slot value.
- Add Medium as the second slot value.
- Add High as the third slot value.
- Select Restrict to slot values.
- Choose Save slot type.
Good progress. Your bot now has a controlled vocabulary for devices, issue categories, and priorities.
Why Restrict These Values?
Restricted resolution helps Lex map demo answers to known values. This reduces ambiguity when you test the conversation.
The description stays unrestricted because a technical problem can be phrased in many different ways.
Add the ReportIssue intent and required slots
An intent represents the user's goal. Sample utterances teach Lex which messages belong to the ReportIssue goal.
- Select Intents from the language menu.
- Choose Add intent.
- Choose the option to create an empty intent.
- Enter ReportIssue as the intent name.
- Create the intent.
The intent editor now needs examples of how a user might ask for technical help.
- Add I need to report an IT issue as a sample utterance.
- Add I need technical help as a sample utterance.
- Add Report a computer problem as a sample utterance.
- Add My device is not working as a sample utterance.
- Add I have a laptop issue as a sample utterance.
Why Vary the Utterances?
Users describe the same goal with different words. Varied examples help Lex connect those phrases to one intent.
- Add I need help with a network problem as a sample utterance.
- Add I have a software issue as a sample utterance.
- Add I need to report a hardware problem as a sample utterance.
- Add I cannot access my account as a sample utterance.
- Add Help me fix a device problem as a sample utterance.
Required slots turn one request into a multi-turn dialog. Lex asks for each missing value in the order you configure.
- Choose Add slot in the Slots section.
- Enter DeviceType as the slot name.
- Select DeviceTypeSlotType as the slot type.
- Mark the slot as required.
- Enter Which device needs help? as the prompt.
- Save the slot.
The first question identifies the device that needs support.
- Choose Add slot.
- Enter IssueCategory as the slot name.
- Select IssueCategorySlotType as the slot type.
- Mark the slot as required.
- Enter What kind of issue are you having? as the prompt.
- Save the slot.
The second question classifies the issue for consistent handling.
- Choose Add slot.
- Enter Description as the slot name.
- Select AMAZON.FreeFormInput as the slot type.
- Mark the slot as required.
- Enter Describe what is happening. as the prompt.
- Save the slot.
The third question captures the user's own description without restricting the wording.
- Choose Add slot.
- Enter Priority as the slot name.
- Select PrioritySlotType as the slot type.
- Mark the slot as required.
- Enter What priority should this ticket have? as the prompt.
- Save the slot.
- Confirm that the slot order is DeviceType, IssueCategory, Description, and Priority.
- Save the ReportIssue intent.
Your intent now defines a four-question intake flow with ten recognition examples.
Build and test the conversation
A closing response gives the user a clear end to the conversation. For this first test, the response stays static.
- Expand the Closing response section in ReportIssue.
- Enter Thanks. I have recorded your issue details. as the response.
- Save the intent.
Building can take a little while because Lex prepares the language model for testing. A short pause on the progress screen is expected.
- Choose Build.
When the build succeeds, the latest intent configuration is ready in the test window.
This test uses five text inputs. The short run keeps your no-more-than-25-text-input allowance under control.
- Choose Test to open the test window.
Before you send the first message, predict whether collecting all four values is enough to produce a traceable ticket.
- Enter I need to report an IT issue in the test window.
- Enter Laptop when the bot asks for the device.
- Enter Network when the bot asks for the issue category.
- Enter Wi-Fi disconnects every few minutes when the bot asks for a description.
- Enter High when the bot asks for the priority.
You will see the static acknowledgement after the fourth answer. The response contains no reference such as INC-demo-001.
- Open Amazon DynamoDB in a separate AWS Management Console tab.
- Select Tables.
- Search the table list for ITHelpDeskTickets.
The table list has no ITHelpDeskTickets table. Your completed conversation collected values without creating a durable ticket.
This Gap Is Intentional
The test proves that Lex can recognize the intent and elicit every required slot. It also proves that a static closing response cannot persist the report.
No AWS Lambda fulfillment hook runs during this conversation. The acknowledgement therefore has no stored record behind it.
Conversation Not Reaching the Closing Response?
Confirm that all four slots are required. Check that each slot has a prompt.
Save the intent before building the bot. Clear the current test session after a new build so Lex uses a fresh conversation.
Help me troubleshoot my Lex conversation.
You now have a working IT intake conversation with a visible backend gap. Next, you will create the durable ticket store and fulfillment boundary.
Create the Ticket Store and Lambda Role
Your built Amazon Lex V2 conversation collects a complete incident report. Its static closing response leaves no durable ticket behind.
This step adds an Amazon DynamoDB table as the system of record. You will also create an AWS Lambda function with permission to write only to that table.
In this step, get ready to:
- Create a fixed-capacity ticket table.
- Create a Node.js 24 fulfillment function.
- Restrict the function to ticket writes.
Create the ticket table
A DynamoDB table gives every incident a durable home. Its partition key uniquely identifies each stored ticket.
- Switch back to the AWS Management Console tab from earlier.
- Confirm the selected AWS Region matches the Region containing ITHelpDeskBot.
- Use the console search bar to find DynamoDB.
- Select DynamoDB from the search results.
- Choose Tables in the left navigation pane.
- Choose Create table.
- Enter ITHelpDeskTickets for Table name.
- Enter ticketId for Partition key.
How Does the Partition Key Work?
A partition key identifies the item DynamoDB should read or update. Using ticketId lets the fulfillment function address each ticket by its reference.
- Select String as the partition key type.
- Leave Sort key empty.
- Choose Customize settings under Table settings.
- Keep DynamoDB Standard as the table class.
- Keep Provisioned for the capacity mode.
- Set read Auto scaling to Off.
- Enter 1 for the read capacity.
- Set write Auto scaling to Off.
Why Use Fixed Capacity?
Provisioned capacity gives this small demo a fixed throughput limit. The table uses 1 RCU.
It also uses 1 WCU. Disabling auto scaling keeps those values fixed.
Creating a cloud table can feel like a cost commitment. These settings keep the resource at the planned project scale.
- Enter 1 for the write capacity.
- Leave point-in-time recovery turned off.
- Leave DynamoDB Streams turned off.
- Choose Create table.
- Wait until the table status shows Active.
- Copy the table ARN into this project variable: your ITHelpDeskTickets table ARN.
That is the storage layer ready. ITHelpDeskTickets can now accept keyed records once the fulfillment code arrives.
Create the fulfillment function
The fulfillment function becomes the controlled bridge between the conversation and the ticket table. Its execution role defines which AWS actions that bridge can perform.
- Use the AWS Management Console search bar to find Lambda.
- Select Lambda from the search results.
- Choose Functions in the left navigation pane.
- Choose Create function.
- Select Author from scratch.
- Enter LexTicketFulfillment for Function name.
- Select Node.js 24.x for Runtime.
- Keep x86_64 for Architecture.
Why Node.js 24?
The next step packages the exact @aws-sdk/client-dynamodb version 3.1138.0. That package requires Node.js 20 or newer.
The Node.js 24.x runtime satisfies that requirement. It also supports the ES module handler used later.
- Expand Change default execution role.
- Select Create a new role with basic Lambda permissions.
- Choose Create function.
The function page now shows LexTicketFulfillment with generated starter code. The function is ready for configuration.
What Does the Basic Role Provide?
An IAM execution role grants the function permission to use AWS resources. The new basic role allows output to Amazon CloudWatch Logs.
The table permission stays separate. You will add one table-specific write action next.
Configure Lambda access
An environment variable gives the function its table name without embedding that value in the handler. A least-privilege policy then limits the role to the required write operation.
- Choose the Configuration tab in LexTicketFulfillment.
- Choose Environment variables in the left panel.
- Choose Edit.
- Choose Add environment variable.
- Enter TABLE_NAME for the key.
- Enter ITHelpDeskTickets for the value.
- Choose Save.
You will see TABLE_NAME listed with the value ITHelpDeskTickets. The function can now read the table name from its environment.
The inline policy is a JSON document that describes the allowed action. Its resource placeholder keeps the source file reusable until you paste it into IAM.
- Create iam-policy.json in the home directory of AWS CloudShell with the built-in editor.
- Paste this policy into iam-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteHelpDeskTickets",
"Effect": "Allow",
"Action": "dynamodb:PutItem",
"Resource": "your-dynamodb-table-arn-here"
}
]
}
What Does This Policy Allow?
- The Action value permits only dynamodb:PutItem.
- The Resource value limits that permission to one table ARN.
- The Effect value grants the scoped action.
- Save iam-policy.json.
- Confirm the saved file still contains your-dynamodb-table-arn-here.
Policy File Not Valid?
Check that every property name remains inside double quotes. Confirm the final property has no trailing comma.
Compare each brace with the reference below. Help me check my IAM policy JSON.
✔️ Awesome, I've got everything!
Great. Double check that iam-policy.json is saved in AWS CloudShell.
ⓧ I'd like to double check the full code
Your complete iam-policy.json file should look exactly like this.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteHelpDeskTickets",
"Effect": "Allow",
"Action": "dynamodb:PutItem",
"Resource": "your-dynamodb-table-arn-here"
}
]
}
- Return to the Configuration tab for LexTicketFulfillment.
- Choose Permissions in the left panel.
- Select the execution role name under Role name.
- Choose the Permissions tab on the IAM role page.
- Choose Add permissions.
- Choose Create inline policy.
- Select the JSON policy editor.
Why Edit the Execution Role?
Lambda assumes this role whenever LexTicketFulfillment runs. A permission attached here becomes available to the function during an invocation.
- Paste the contents of iam-policy.json into the policy editor.
- Replace your-dynamodb-table-arn-here with your ITHelpDeskTickets table ARN.
- Continue to the policy review screen.
- Enter WriteHelpDeskTickets for the policy name.
- Choose Create policy.
The execution role now lists WriteHelpDeskTickets as an inline policy. Its single permitted database action targets only ITHelpDeskTickets.
Why Is This Least Privilege?
The role can add an item through dynamodb:PutItem. It cannot read tickets or delete them.
The table ARN narrows the permission to ITHelpDeskTickets. Other DynamoDB tables remain outside the role's scope.
- Return to LexTicketFulfillment in the Lambda console.
- Choose the Test tab.
- Choose Create new event under Test event.
- Replace the event JSON with this empty document:
{}
What Does This Test Event Do?
The generated starter handler does not require input fields. An empty JSON document is enough to invoke it and confirm the function boundary works.
Before you test it, do you think the starter function can run with its new configuration?
- Run the unsaved event by choosing Test.
You will see a successful execution with the generated starter response. This proves the function and its basic execution role can run.
The starter code still creates no DynamoDB item. The ticket-writing implementation arrives in the next step.
Starter Test Failing?
Confirm the function runtime is Node.js 24.x. Check that the generated starter code remains unchanged.
Confirm the function finished creating before you test it again. Help me diagnose my Lambda starter test.
Your ticket store and fulfillment boundary are ready. Next, you will package the handler that turns a Lex conversation into a stored ticket reference.
Package the Fulfillment Function
The previous step gave your workflow a durable DynamoDB table. It also gave your Lambda function permission to write one item at a time.
The static Amazon Lex V2 response still finishes the conversation without creating evidence. In this step, you package fulfillment code that stores the collected slots. The function then returns a ticket reference.
In this step, get ready to:
- Create the pinned deployment files in AWS CloudShell.
- Upload the deployment archive to LexTicketFulfillment.
- Prove that the function returns and stores a ticket.
Create the deployment package
A deployment package combines your function source with the exact libraries it needs. You will assemble that package in AWS CloudShell so the files stay in the selected AWS Region's browser-based environment.
- Switch back to the CloudShell terminal from earlier.
- Create the `lex-it-help-desk` directory by running these commands:
mkdir lex-it-help-desk
cd lex-it-help-desk
What do these commands do?
- The `mkdir` command creates a dedicated directory for the deployment files.
- The `cd` command moves your terminal into that directory.
The package manifest pins the AWS SDK for JavaScript dependency. This keeps the function tied to the tested `3.1138.0` release.
- Create `package.json` in the CloudShell editor by running this command:
edit package.json
What does this command do?
The `edit` command opens `package.json` in CloudShell's built-in editor. The file is created when it does not already exist.
- Paste this package manifest into `package.json`:
{
"name": "lex-it-help-desk",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"@aws-sdk/client-dynamodb": "3.1138.0"
}
}
What does this file control?
- The `type` value enables the module syntax used by the function source.
- The dependency entry installs the low-level DynamoDB client at the exact tested version.
- The `private` value prevents accidental publication as a public package.
- Save `package.json` by pressing **Cmd+S** on macOS or **Ctrl+S** on Windows.
- Return to the CloudShell terminal.
- Install the dependency declared in `package.json` by running:
npm install
What does this install?
The command reads `package.json`. It installs `@aws-sdk/client-dynamodb` inside `node_modules` for inclusion in the deployment archive.
Packaging the client gives the Lambda function a reproducible SDK version. The archive carries the same dependency you tested.
CloudShell returns to the prompt after the dependency is installed. You should see installation output without dependency errors.
Dependency install failing?
Confirm that `package.json` contains valid JSON. A missing quote or comma stops `npm` from reading the dependency list.
Confirm that CloudShell reports Node.js `20.0.0` or newer. `@aws-sdk/client-dynamodb` version `3.1138.0` requires that minimum.
Ask for help with the installation output: Help me troubleshoot npm install in AWS CloudShell.
- Create `index.mjs` in the CloudShell editor by running this command:
edit index.mjs
Why use index.mjs?
The `.mjs` extension identifies the file as an ES module. The Lambda handler setting points to the exported `handler` function inside this file.
- Paste the client setup and slot helper into `index.mjs`:
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
const tableName = process.env.TABLE_NAME;
function slotValue(slots, name) {
return slots?.[name]?.value?.interpretedValue ?? "Not provided";
}
How does the function read its inputs?
- `DynamoDBClient` gives the function a client for sending table commands.
- `tableName` reads the existing `TABLE_NAME` environment variable.
- `slotValue()` extracts an interpreted Lex slot value. It supplies `Not provided` when the value is absent.
- Paste the response helper below `slotValue()`:
function closeResponse(intent, state, content) {
return {
sessionState: {
dialogAction: {
type: "Close"
},
intent: {
...intent,
state
}
},
messages: [
{
contentType: "PlainText",
content
}
]
};
}
How does the response helper work?
- `closeResponse()` builds the response shape expected by Lex V2.
- The intent state records whether fulfillment succeeded or failed.
- The plain-text message becomes the final reply shown to the user.
- Save `index.mjs` by pressing **Cmd+S** on macOS or **Ctrl+S** on Windows.
- Return to the CloudShell terminal.
- Check the saved helpers for JavaScript syntax errors by running:
node --check index.mjs
What does this check prove?
The `--check` option asks Node.js to parse `index.mjs` without executing it. A return to the prompt without a syntax error confirms that the first two helpers are valid.
Seeing a syntax error?
Check the closing braces in `closeResponse()`. Each nested object must close before the final function brace.
Compare the quote marks around `Close` and `PlainText`. Straight double quotes keep the JavaScript valid.
Ask for help with the reported line: Help me fix this JavaScript syntax error.
The handler is longer than one paste chunk. Add both parts in order before saving the file.
- Return to the open `index.mjs` editor tab.
- Paste the first handler section below `closeResponse()`:
export const handler = async (event) => {
const intent = event.sessionState.intent;
if (intent.name !== "ReportIssue") {
return closeResponse(intent, "Failed", "I cannot handle that request yet.");
}
const slots = intent.slots ?? {};
const ticketId = `INC-${event.sessionId}`;
const item = {
ticketId: { S: ticketId },
deviceType: { S: slotValue(slots, "DeviceType") },
issueCategory: { S: slotValue(slots, "IssueCategory") },
description: { S: slotValue(slots, "Description") },
priority: { S: slotValue(slots, "Priority") },
status: { S: "OPEN" },
createdAt: { S: new Date().toISOString() }
};
How is the ticket assembled?
- The intent check limits this handler to `ReportIssue` requests.
- The `sessionId` becomes a demo ticket reference with the `INC-` prefix.
- The `item` map converts each collected slot into a DynamoDB string attribute.
- Every new ticket receives the `OPEN` status and an ISO timestamp.
- Paste the remaining handler section directly below the `item` object:
try {
await client.send(
new PutItemCommand({
TableName: tableName,
Item: item
})
);
console.log(JSON.stringify({ ticketId, status: "stored" }));
return closeResponse(
intent,
"Fulfilled",
`Ticket ${ticketId} was created with ${item.priority.S} priority.`
);
} catch (error) {
console.error("Ticket creation failed", error);
return closeResponse(
intent,
"Failed",
"I could not create the ticket. Please try again later."
);
}
};
How is the ticket stored?
- `PutItemCommand` sends the completed item to the table named by `TABLE_NAME`.
- A successful write logs the ticket reference with the `stored` state.
- The fulfilled response includes the ticket reference and selected priority.
- The catch block returns a controlled failure message if the write fails.
- Save `index.mjs` by pressing **Cmd+S** on macOS or **Ctrl+S** on Windows.
- Return to the CloudShell terminal.
- Check the completed handler for JavaScript syntax errors by running:
node --check index.mjs
What does this check confirm?
Node.js now parses the complete source file. A return to the prompt without a syntax error confirms that the handler is ready to package.
Handler syntax check failing?
Confirm that the second handler section sits inside the `handler` function. Its `try` block starts immediately after the `item` object.
Check the backticks around both ticket messages. Curly quotes prevent template values from working.
Ask for help with the failing line: Help me repair my fulfillment handler.
A fixed test event gives the function the same four values every time. This makes the expected ticket reference and stored attributes predictable.
- Create `lex-fulfillment-test.json` in the CloudShell editor by running this command:
edit lex-fulfillment-test.json
Why save the test event?
The saved JSON gives you a reusable copy of the fulfillment event. It represents a laptop network issue with high priority.
The event is one JSON document. Paste both portions in order before saving the file.
- Paste the first event portion into `lex-fulfillment-test.json`:
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook",
"inputMode": "Text",
"sessionId": "demo-001",
"sessionState": {
"intent": {
"name": "ReportIssue",
"slots": {
"DeviceType": {
"value": {
"interpretedValue": "Laptop"
}
},
"IssueCategory": {
"value": {
"interpretedValue": "Network"
}
},
What does this event identify?
The event identifies a Lex V2 fulfillment request for `ReportIssue`. Its `demo-001` session produces the predictable `INC-demo-001` ticket reference.
- Paste the remaining event portion directly below the final comma:
"Description": {
"value": {
"interpretedValue": "Wi-Fi disconnects every few minutes"
}
},
"Priority": {
"value": {
"interpretedValue": "High"
}
}
}
}
}
}
What values complete the event?
The remaining slots supply the issue description and priority. The closing braces complete the nested intent structure.
- Save `lex-fulfillment-test.json` by pressing **Cmd+S** on macOS or **Ctrl+S** on Windows.
- Return to the CloudShell terminal.
Before you create the archive, picture the source files and installed dependency that should appear in the packaging output.
- Create `my_deployment_package.zip` from the current directory by running:
zip -r my_deployment_package.zip .
What does this archive contain?
The recursive `zip` command adds the project contents at the archive root. The output should list `package.json`, `index.mjs`, `lex-fulfillment-test.json`, and files under `node_modules`.
Keeping `index.mjs` at the archive root allows the `index.handler` setting to find the exported function.
Archive creation failing?
Confirm that the terminal prompt is inside `lex-it-help-desk`. Running the command elsewhere packages the wrong files.
Confirm that `npm install` completed before creating the archive. The deployment needs the installed SDK files under `node_modules`.
Ask for help with the archive output: Help me fix my Lambda deployment archive.
Upload the package to Lambda
The archive currently lives inside CloudShell. Downloading it to your Mac gives the Lambda console a local file to upload.
- Switch CloudShell to its full-screen view.
- Choose **Actions** in CloudShell.
- Choose **Download file**.
- Enter `my_deployment_package.zip` as the relative path.
- Choose **Download**.
Your browser saves `my_deployment_package.zip` to the Mac's configured download location.
- Return to `LexTicketFulfillment` in the Lambda console from earlier.
- Select **Code**.
- Select **Upload from**.
- Select **.zip file**.
- Choose **Upload**.
- Select `my_deployment_package.zip` in the Mac file picker.
- Choose **Open**.
- Choose **Save**.
The function's code source now comes from your deployment archive. Lambda keeps the existing `TABLE_NAME` value and execution-role permissions.
- Find the **Handler** value in the function's code configuration.
- Set the handler value to `index.handler` if it differs.
The handler now points to the `handler` export in `index.mjs`.
Package upload failing?
Confirm that you selected `my_deployment_package.zip`. Uploading the project directory directly does not provide Lambda with a deployment archive.
Download the archive again from CloudShell if the browser reports an incomplete file. Keep CloudShell in full-screen view so **Download file** remains available.
Ask for help with the upload step: Help me upload my Lambda zip package.
Run the fulfillment test
The test event simulates the fulfillment payload Lex sends after collecting all four slots. A successful invocation must return a reference and create the matching table item.
- Find the **TEST EVENTS** section in the Lambda console code editor.
- Choose **Create test event**.
- Enter `lex-fulfillment-test` for **Event Name**.
- Replace the default content in **Event JSON** with the first portion below:
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook",
"inputMode": "Text",
"sessionId": "demo-001",
"sessionState": {
"intent": {
"name": "ReportIssue",
"slots": {
"DeviceType": {
"value": {
"interpretedValue": "Laptop"
}
},
"IssueCategory": {
"value": {
"interpretedValue": "Network"
}
},
What does this test select?
This portion selects the `ReportIssue` intent. It supplies the device type and issue category collected during the conversation.
- Paste the remaining event portion directly below the final comma:
"Description": {
"value": {
"interpretedValue": "Wi-Fi disconnects every few minutes"
}
},
"Priority": {
"value": {
"interpretedValue": "High"
}
}
}
}
}
}
What completes the test?
The description and priority complete the ticket fields. The event now contains every slot required by the handler.
- Choose **Save** to store the test event.
Before you run the event, picture the ticket reference that should come from the `demo-001` session.
- Choose the run icon beside `lex-fulfillment-test` in the **TEST EVENTS** section.
The invocation completes successfully. The response contains `Ticket INC-demo-001 was created` with `High` priority.
What did the invocation prove?
The handler converted `demo-001` into `INC-demo-001`. It sent the ticket item to the table before returning the fulfilled response.
The invocation also writes `{"ticketId":"INC-demo-001","status":"stored"}` to CloudWatch Logs. That log entry provides a trace of the successful write.
- Return to the DynamoDB console from earlier.
- Select `ITHelpDeskTickets`.
- Choose **Explore table items**.
- Find the item with `ticketId` set to `INC-demo-001`.
You should see `INC-demo-001` with `Laptop`, `Network`, `High`, and `OPEN`. The stored description reads `Wi-Fi disconnects every few minutes`.
Test failed or item missing?
Confirm that the handler is `index.handler`. A different value prevents Lambda from finding the exported function.
Confirm that `TABLE_NAME` still contains `ITHelpDeskTickets`. The function uses that environment variable for `PutItemCommand`.
Confirm that the execution role still allows `dynamodb:PutItem` for the `ITHelpDeskTickets` table ARN. Help me diagnose the failed ticket write.
✔️ Awesome, I've got everything!
Your deployment package is uploaded. The test event now returns `INC-demo-001` and stores the matching `OPEN` ticket.
ⓧ I'd like to double check the full code
Your `package.json` file should match this reference.
{
"name": "lex-it-help-desk",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"@aws-sdk/client-dynamodb": "3.1138.0"
}
}
What should match?
The dependency name and exact version must match. The module type must remain `module`.
Your complete `index.mjs` file should match this reference.
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
const tableName = process.env.TABLE_NAME;
function slotValue(slots, name) {
return slots?.[name]?.value?.interpretedValue ?? "Not provided";
}
function closeResponse(intent, state, content) {
return {
sessionState: {
dialogAction: {
type: "Close"
},
intent: {
...intent,
state
}
},
messages: [
{
contentType: "PlainText",
content
}
]
};
}
export const handler = async (event) => {
const intent = event.sessionState.intent;
if (intent.name !== "ReportIssue") {
return closeResponse(intent, "Failed", "I cannot handle that request yet.");
}
const slots = intent.slots ?? {};
const ticketId = `INC-${event.sessionId}`;
const item = {
ticketId: { S: ticketId },
deviceType: { S: slotValue(slots, "DeviceType") },
issueCategory: { S: slotValue(slots, "IssueCategory") },
description: { S: slotValue(slots, "Description") },
priority: { S: slotValue(slots, "Priority") },
status: { S: "OPEN" },
createdAt: { S: new Date().toISOString() }
};
try {
await client.send(
new PutItemCommand({
TableName: tableName,
Item: item
})
);
console.log(JSON.stringify({ ticketId, status: "stored" }));
return closeResponse(
intent,
"Fulfilled",
`Ticket ${ticketId} was created with ${item.priority.S} priority.`
);
} catch (error) {
console.error("Ticket creation failed", error);
return closeResponse(
intent,
"Failed",
"I could not create the ticket. Please try again later."
);
}
};
What should match?
Check every identifier and slot name carefully. The handler must keep the `ReportIssue` intent check and the `OPEN` status.
Your `lex-fulfillment-test.json` file should match this reference.
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook",
"inputMode": "Text",
"sessionId": "demo-001",
"sessionState": {
"intent": {
"name": "ReportIssue",
"slots": {
"DeviceType": {
"value": {
"interpretedValue": "Laptop"
}
},
"IssueCategory": {
"value": {
"interpretedValue": "Network"
}
},
"Description": {
"value": {
"interpretedValue": "Wi-Fi disconnects every few minutes"
}
},
"Priority": {
"value": {
"interpretedValue": "High"
}
}
}
}
}
}
What should match?
The session ID must remain `demo-001`. The four slot names must match the handler's lookups exactly.
Your existing `iam-policy.json` file remains unchanged.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteHelpDeskTickets",
"Effect": "Allow",
"Action": "dynamodb:PutItem",
"Resource": "your-dynamodb-table-arn-here"
}
]
}
What should remain unchanged?
The policy continues to grant only `dynamodb:PutItem`. Its resource value represents the table ARN configured in the previous step.
That closes the missing-backend gap. Your function can now create a traceable ticket. Next, you will connect it to the live Lex conversation.
Connect Lex to Lambda
Your AWS Lambda fulfillment function already turns a test event into a stored Amazon DynamoDB ticket.
Now you will connect Amazon Lex V2 to that function so a real conversation can trigger the workflow. The function joins the conversation only after you attach it to the bot alias and enable the successful fulfillment hook.
In this step, get ready to:
- Attach LexTicketFulfillment to the test alias.
- Rebuild ITHelpDeskBot with its Lambda fulfillment hook enabled.
- Capture matching end-to-end evidence for one returned ticket reference.
Attach the function to the test alias
Amazon Lex V2 associates a Lambda function with a bot alias for each language. This gives the test alias a function to invoke during successful fulfillment.
- Return to ITHelpDeskBot in the Amazon Lex V2 console.
- Open the bot's test alias.
- Open the English (US) language settings for the alias.
- Select LexTicketFulfillment in Lambda function version or alias.
- Choose Save.
- Confirm LexTicketFulfillment remains selected for English (US).
That completes the alias connection. English requests can now reach LexTicketFulfillment when an intent invokes the hook.
Function Not Listed?
- Confirm ITHelpDeskBot uses your selected AWS Region.
- Confirm LexTicketFulfillment uses your selected AWS Region.
- Refresh the alias settings before checking the function selector again.
Help me troubleshoot the missing Lambda function.
Enable fulfillment and rebuild the bot
The alias connection makes the function available to the bot. The ReportIssue intent still controls whether successful fulfillment invokes it.
- Return to the ReportIssue intent in the English (US) locale.
- Open Fulfillment.
- Turn on Active.
- Open On successful fulfillment.
- Choose Advanced options.
- Select Use a Lambda function for fulfillment.
- Save the intent.
The ReportIssue intent now has an active path to the function. Rebuilding publishes that configuration to the test environment.
- Rebuild ITHelpDeskBot by choosing Build.
- Wait for the build to finish.
- Confirm the build completes without an error.
The rebuilt bot now carries the Lambda fulfillment configuration into its next test conversation.
Build Not Completing?
- Confirm the test alias association was saved for English (US).
- Confirm Active remains enabled under Fulfillment.
- Save the ReportIssue intent before starting another build.
Help me troubleshoot the Lex build.
Demonstrate the complete workflow
The final test follows one reference across the complete workflow. You will begin with the Lex response.
The same reference must appear in DynamoDB. It must also appear in CloudWatch Logs.
Before you start the conversation, pause on one question: do you expect the old static acknowledgement or a new ticket reference? Make your prediction now.
- Open the test window for ITHelpDeskBot.
- Keep this demonstration inside your allowance of no more than 25 text inputs.
- Type I need to report an IT issue.
- Enter Laptop for DeviceType.
- Enter Network for IssueCategory.
- Enter Wi-Fi disconnects every few minutes for Description.
- Enter High for Priority.
- Record the returned ticket reference here: your returned ticket reference.
You will see a final response containing your returned ticket reference. The static acknowledgement has been replaced by the Lambda response.
That completes the live integration. Your conversation now creates a durable ticket.
- Capture a portfolio screenshot that shows the final response with its returned ticket reference.
The returned response proves that Lex received the function output. The stored item proves that the ticket reached DynamoDB.
- Return to the ITHelpDeskTickets table in Amazon DynamoDB.
- Choose Explore table items.
- Find the item whose ticketId matches your returned ticket reference.
- Confirm its status value is OPEN.
- Capture a portfolio screenshot showing the matching ticketId.
- Include the OPEN status in the same frame.
The matching item proves that the conversation created a persistent ticket record.
The log entry provides the execution trace for the same request. Its stored value confirms that the write finished inside the function.
- Return to the CloudWatch Logs group associated with LexTicketFulfillment.
- Open the log stream created by your end-to-end conversation.
- Find the stored entry for your returned ticket reference.
- Capture a portfolio screenshot of that log entry.
You have now proved the complete workflow from conversation to stored ticket. The chat response carries the same reference as the DynamoDB item.
The CloudWatch log closes the trace with its stored entry.
Secret mission
Add Ticket Status Lookup
Extend your service desk bot with a ticket status lookup. Users will enter an existing ticket reference and receive its stored status from DynamoDB.
Clean Up Your Resources
Clean Up Your Resources
Your service desk bot uses AWS resources that can remain active after you finish testing. Choose whether to keep them available, pause new activity, or delete them entirely.
Cost warning
Amazon Lex V2 charges $0.00075 for each text input. Keeping your testing allowance to no more than 25 text inputs holds the planned Lex request cost below $0.02.
Your AWS Lambda, Amazon DynamoDB, and Amazon CloudWatch Logs usage can remain within their published free-tier allowances at this project's scale. Existing usage in your AWS account also counts toward those allowances.
Pausing ends new Lex requests. The provisioned DynamoDB table can still incur charges when your available free-tier allowance is already consumed, so choose Pause or Delete if you have finished demonstrating the workflow.
Resources you used:
- An Amazon Lex V2 bot named ITHelpDeskBot with its test alias and two fulfillment-enabled intents.
- An AWS Lambda function named LexTicketFulfillment.
- An Amazon DynamoDB table named ITHelpDeskTickets with provisioned capacity of 1 RCU and 1 WCU.
- An AWS Identity and Access Management (IAM) execution role with an inline policy for dynamodb:GetItem and dynamodb:PutItem.
- A CloudWatch log group containing the fulfillment logs for LexTicketFulfillment.
- An AWS CloudShell project directory containing the deployment files and archive from Step 4.
Keep everything running
No action is needed. Choose this option if you want to keep demonstrating ticket creation and status lookup.
- Keep ITHelpDeskBot available for future text demonstrations.
- Keep LexTicketFulfillment available for ticket creation and status lookup.
- Retain the records in ITHelpDeskTickets as demonstration data.
- Retain the CloudWatch logs as evidence of successful fulfillment.
- Stop testing when you reach the allowance of no more than 25 text inputs.
- Monitor your AWS billing information for usage outside the available free-tier allowances.
Your bot remains ready to accept requests. Each additional text input can add to your Amazon Lex charges.
Pause - I'll come back to this later
Pausing means stopping new requests while preserving the workflow for later. The function and table remain configured in your selected Region.
- Stop sending new text inputs to ITHelpDeskBot.
- Leave LexTicketFulfillment idle.
- Leave ITHelpDeskTickets at its current provisioned capacity.
- Return to CloudWatch Logs in your selected Region.
- Select the log group whose recent stream contains the stored entry from LexTicketFulfillment.
- Delete old log streams that you no longer need.
This preserves the complete bot configuration. DynamoDB capacity can still incur charges when your account has no remaining free-tier allowance.
Delete - I don't want to use this again
Deleting these resources is permanent. Each confirmation below identifies the exact project resource being removed, so you can clean up without affecting unrelated AWS work.
Bot cleanup:
- Return to the Amazon Lex V2 bot list in your selected Region.
- Select ITHelpDeskBot.
- Choose Delete for the selected bot.
- Complete the deletion confirmation for ITHelpDeskBot.
- Confirm ITHelpDeskBot no longer appears in the bot list.
Function cleanup:
- Return to LexTicketFulfillment in AWS Lambda.
- Record the function's execution role name: your Lambda execution role name.
- Return to the AWS Lambda function list.
- Select LexTicketFulfillment.
- Choose Delete for the selected function.
- Complete the deletion confirmation for LexTicketFulfillment.
- Confirm LexTicketFulfillment no longer appears in the function list.
Permissions cleanup:
- Return to Roles in IAM.
- Select your Lambda execution role name.
- Open the role's Permissions page.
- Select the inline policy that permits dynamodb:GetItem and dynamodb:PutItem on ITHelpDeskTickets.
- Choose Delete for the selected inline policy.
- Complete the policy deletion confirmation.
- Return to the execution role details.
- Choose Delete for the execution role.
- Complete the role deletion confirmation.
- Confirm your Lambda execution role name no longer appears in Roles.
Ticket data cleanup:
- Return to the Amazon DynamoDB table list in your selected Region.
- Select ITHelpDeskTickets.
- Choose Delete for the selected table.
- Complete the deletion confirmation for ITHelpDeskTickets.
- Confirm ITHelpDeskTickets no longer appears in the table list.
Log cleanup:
- Return to the CloudWatch log groups in your selected Region.
- Select the log group whose streams contain fulfillment entries for LexTicketFulfillment.
- Choose Delete for the selected log group.
- Complete the log group deletion confirmation.
- Confirm the matching log group no longer appears in CloudWatch Logs.
CloudShell cleanup:
- Switch back to AWS CloudShell from earlier.
- Remove the CloudShell project directory from Step 4 by entering its exact path in the command below:
cd ~
rm -rf [[CLOUDSHELL_PROJECT_DIRECTORY="your CloudShell project directory from Step 4"]]
What does this command remove?
The first line returns CloudShell to its home directory. The second line permanently removes only the project directory path you entered.
- Confirm CloudShell returns to the prompt without reporting an error.
- Confirm the project directory from Step 4 is no longer present.
CloudShell directory still present?
- Check that the entered path matches the CloudShell project directory from Step 4 exactly.
- Help me safely remove my CloudShell project directory.
You have cleared every project resource from the selected Region. The bot can no longer accept requests or create tickets.
Nice Work!
Nice Work!
You did it! Your service desk bot now turns an IT issue into a stored ticket with a traceable reference.
You've learned how to:
- Built a multi-turn Amazon Lex V2 conversation that gathers a complete IT incident report. Proved that conversational capture alone does not create a durable ticket.
- Packaged a pinned AWS SDK for JavaScript dependency inside an AWS Lambda deployment. Used least-privilege IAM permissions to write OPEN records to Amazon DynamoDB.
- Connected the bot alias to the fulfillment function. Verified each returned ticket reference in Amazon DynamoDB. Traced the matching invocation in Amazon CloudWatch Logs.
- Secret Mission: Added the CheckTicketStatus intent to retrieve the stored status for an existing ticket. Returned a clear response when a ticket reference did not exist.
Ready to quiz yourself?