Runtime Conversation Handling
Sending/receiving runtime messages and managing session state via SDK.
Search across all documentation pages
Sending/receiving runtime messages and managing session state via SDK.
Once a bot is built and published, the client side of a conversation is just a sequence of RecognizeText (or RecognizeUtterance) calls, one per user turn, all sharing a sessionId. This page covers driving that loop from your own application code: sending a turn, reading back the intent and slot state, and reacting correctly to what Lex wants to happen next.
Quick-reference recipe card - copy-paste ready.
# --- Python (boto3) ---
resp = lex_runtime.recognize_text(
botId=bot_id, botAliasId=alias_id, localeId="en_US",
sessionId=session_id, text=user_text,
)
dialog_type = resp["sessionState"]["dialogAction"]["type"] # ElicitSlot / ConfirmIntent / Delegate / Close
for m in resp.get("messages", []):
print(m["content"])// --- TypeScript (AWS SDK v3) ---
const resp = await lexRuntime.send(new RecognizeTextCommand({
botId: botId, botAliasId: aliasId, localeId: "en_US",
sessionId: sessionId, text: userText,
}));
const dialogType = resp.sessionState?.dialogAction?.type; // ElicitSlot / ConfirmIntent / Delegate / Close
for (const m of resp.messages ?? []) console.log(m.content);When to reach for this:
RecognizeUtterance instead when the input is audio rather than text.A minimal conversation loop: send a turn, branch on dialogAction.type, and keep going with the same sessionId until the intent closes.
# --- Python (boto3) ---
import boto3
import uuid
lex_runtime = boto3.client("lexv2-runtime", region_name="us-east-1")
session_id = str(uuid.uuid4()) # one per conversation, stable across turns
def send_turn(text: str) -> dict:
return lex_runtime.recognize_text(
botId="BOTID123", botAliasId="prod-alias-id", localeId="en_US",
sessionId=session_id, text=text,
)
resp = send_turn("I'd like to order a pizza")
for m in resp.get("messages", []):
print("Bot:", m["content"])
state = resp["sessionState"]
if state["dialogAction"]["type"] == "ElicitSlot":
slot_name = state["dialogAction"]["slotToElicit"]
print(f"(Lex is waiting on slot: {slot_name})")
resp = send_turn("large") # answer the elicited slot in the next turn
intent = resp["sessionState"]["intent"]
print("Intent:", intent["name"], "| state:", intent["state"])
for slot_name, slot in (intent.get("slots") or {}).items():
if slot:
print(f" {slot_name} = {slot['value']['interpretedValue']} (said: {slot['value']['originalValue']})")// --- TypeScript (AWS SDK v3) ---
import { LexRuntimeV2Client, RecognizeTextCommand } from "@aws-sdk/client-lex-runtime-v2";
import { randomUUID } from "node:crypto";
const lexRuntime = new LexRuntimeV2Client({ region: "us-east-1" });
const sessionId = randomUUID(); // one per conversation, stable across turns
async function sendTurn(text: string) {
return lexRuntime.send(new RecognizeTextCommand({
botId: "BOTID123", botAliasId: "prod-alias-id", localeId: "en_US",
sessionId, text,
}));
}
let resp = await sendTurn("I'd like to order a pizza");
for (const m of resp.messages ?? []) console.log("Bot:", m.content);
let state = resp.sessionState!;
if (state.dialogAction?.type === "ElicitSlot") {
const slotName = state.dialogAction.slotToElicit;
console.log(`(Lex is waiting on slot: ${slotName})`);
resp = await sendTurn("large"); // answer the elicited slot in the next turn
}
const intent = resp.sessionState!.intent!;
console.log("Intent:", intent.name, "| state:", intent.state);
for (const [slotName, slot] of Object.entries(intent.slots ?? {})) {
if (slot) console.log(` ${slotName} = ${slot.value?.interpretedValue} (said: ${slot.value?.originalValue})`);
}What this demonstrates:
sessionId - that's the entire mechanism Lex uses to thread turns into one conversation.dialogAction.type drives your client's next action: ElicitSlot means show the prompt and wait for the answer to that specific slot.messages is the ordered list of things to say/show for the current turn - always render it, even mid-dialog.slot.value.interpretedValue; slot.value.originalValue is the verbatim user text.RecognizeText takes a plain text string and returns JSON - the natural fit for chat widgets and messaging integrations. RecognizeUtterance takes an audio stream (and returns audio back if the bot has voice configured), for telephony and voice-assistant channels; it takes the same botId/botAliasId/localeId/sessionId shape but with audio content types on the wire (its request/response fields are base64/blob-encoded JSON rather than plain fields, since it's built for streaming clients). Pick based on the channel, not the bot - the same bot definition serves both.
Lex does track session state server-side for the session's TTL window, but the response you get back is the authoritative "next" state - your client's job is mainly to react to it (show prompts, collect the next answer), not to reconstruct it by hand. Where you do need to carry state explicitly is sessionAttributes: anything you want available across turns beyond what Lex tracks natively (a user ID, a computed total) has to be included in an outgoing sessionState if you're overriding it, or it flows through automatically from the prior response if you're not touching it.
ElicitSlot - Lex wants a value for dialogAction.slotToElicit; your next turn should answer that.ConfirmIntent - Lex wants a yes/no before fulfilling; a "yes" turn confirms, and intent.confirmationState reflects the outcome.Delegate - Lex is proceeding on its own (used more inside code hooks than in client responses).Close - the turn is done; intent.state (Fulfilled or Failed) tells you the outcome.Beyond the top-level sessionState.intent, the response's interpretations list ranks alternate intent matches with nluConfidence.score - useful for logging low-confidence matches or building your own confirmation logic on top of Lex's default confidence threshold.
sessionId every call - Lex sees each turn as a brand-new conversation with no memory. Fix: generate one sessionId per conversation and reuse it for every turn.dialogAction.type and always treating the response as final - a mid-dialog ElicitSlot response gets mishandled as a finished turn. Fix: branch on the type before deciding the conversation is done.slot.value as a plain string - it's an object (originalValue/interpretedValue/resolvedValues), not a scalar. Fix: read .interpretedValue for the normalized value.sessionAttributes when overriding session state - custom cross-turn data silently disappears. Fix: if you send an explicit sessionState, include the attributes you want kept.Failed intent state - a fulfillment error looks the same as success if you only check for Close. Fix: check intent.state (Fulfilled vs Failed) after a Close.| Approach | Use When | Don't Use When |
|---|---|---|
RecognizeText (this page) | Text-based chat channels (web, messaging) | The input is audio |
RecognizeUtterance | Voice/telephony channels needing audio in and/or out | A text channel already fits |
| Amazon Connect native Lex integration | You're building an IVR/contact-center flow already on Connect | You're driving the bot from your own application code |
| A messaging-platform Lex channel integration (where available) | The platform has a built-in Lex connector | You need custom pre/post-processing per turn |
botId, botAliasId, localeId, a sessionId you control, and the text of the current turn. Everything else (prior state) is threaded through by sessionId.
sessionState.dialogAction.type is "Close" and sessionState.intent.state is "Fulfilled" or "Failed".
sessionState.intent.slots.<SlotName>.value.interpretedValue for the normalized value, or .originalValue for exactly what the user typed/said.
No - unlike a stateless LLM chat API, Lex tracks session state server-side keyed by sessionId for its TTL window. You mainly need to carry forward sessionAttributes if you're setting them explicitly.
Both share botId/botAliasId/localeId/sessionId. RecognizeText takes/returns plain JSON with a text field; RecognizeUtterance is built for streaming audio content and returns audio as well when the bot has voice enabled.
The response's interpretations list, ordered by nluConfidence.score - useful for logging or building custom confidence handling on top of Lex's default threshold.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Reviewed by Chris St. John·Last updated Jul 25, 2026