OpenDialog Multi-Turn Agent
This integration calls an OpenDialog Webchat Chat API scenario and preserves the OpenDialog user session across turns within an evaluation session. It is the conversational counterpart to the OpenDialog Single-Turn Agent.
Required secrets
| Secret | What it is |
|---|---|
OD_API_URL | OpenDialog tenant host or base URL, for example your-tenant.cloud.opendialog.ai. |
OD_SCENARIO_ID | OpenDialog scenario ID to route chat messages to. |
OD_API_TOKEN | Bearer token/app key for the OpenDialog Webchat Chat API. |
Where to get the credentials
From your OpenDialog tenant: use your tenant host for OD_API_URL, the id of the
scenario you want to test for OD_SCENARIO_ID, and a Webchat Chat API token for
OD_API_TOKEN. Store all three as project secrets.
Turn support
Multi-turn. The OpenDialog user session is preserved across turns within a session, so it suits multi-turn evaluation.
Limitations and notes
- The scenario must be reachable through the Webchat Chat API with the supplied token.
How the agent works
The integration you copy from the registry is a single JavaScript function. This walkthrough follows the real code so you can use the same pattern to build your own custom agent connection. It shares almost all of its structure with the OpenDialog Single-Turn Agent; the differences are in how the user session is reused across turns.
The entry point is an async process(input) function, where input is the
user's message text for the current turn. Secrets are read from the ambient
env object by name — env.OD_API_URL, env.OD_SCENARIO_ID, and
env.OD_API_TOKEN, the same keys listed in
Required secrets — and the input is trimmed to a string:
export async function process(input) {
try {
const apiUrl = normalizeApiUrl(env.OD_API_URL);
const scenarioId = String(env.OD_SCENARIO_ID ?? "").trim();
const apiToken = String(env.OD_API_TOKEN ?? "").trim();
const text = String(input ?? "").trim();
// ...
If any secret or the input is missing, it returns an explanatory string instead of calling out:
if (!apiUrl || !scenarioId || !apiToken || !text) {
return "Missing env vars or input: OD_API_URL, OD_SCENARIO_ID, OD_API_TOKEN, input";
}
The key difference from the single-turn agent is module-level state that survives between calls. Two variables hold the active user id and whether the session has already been started:
let activeUserId = "";
let activeSessionStarted = false;
Where the single-turn agent generates a fresh userId on every call, this agent
generates it once and then reuses it. userIdForRequest only creates a new id
when there is no active one, so every later turn keeps the same OpenDialog user
session:
function userIdForRequest() {
if (activeUserId) {
return activeUserId;
}
const convId = generateConversationId();
activeUserId = `${OD_TEST_USER}${convId}`;
return activeUserId;
}
The welcome trigger is likewise sent only once. It is guarded by
activeSessionStarted, which is flipped to true after the first trigger so
later turns skip straight to sending the text message:
const userId = userIdForRequest();
if (!activeSessionStarted) {
await odPost({
apiToken, apiUrl, scenarioId, userId,
payload: buildPayload({
scenarioId, userId, type: "trigger", text: "Welcome", callbackId: "WELCOME",
}),
});
activeSessionStarted = true;
}
const raw = await odPost({
apiToken, apiUrl, scenarioId, userId,
payload: buildPayload({ scenarioId, userId, type: "text", text }),
});
Everything else matches the single-turn agent. Each request is a POST to
${apiUrl}/chat-api/message with the routing carried in headers — a Bearer
token in Authorization, plus OPENDIALOG-SCENARIO-ID and
OPENDIALOG-USER-ID:
const res = await fetch(`${apiUrl}/chat-api/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${apiToken}`,
"OPENDIALOG-SCENARIO-ID": scenarioId,
"OPENDIALOG-USER-ID": userId,
},
body: JSON.stringify(payload),
});
The response is normalized into a list of messages, and extractText keeps only
the text messages and joins their text into the reply that is returned:
function extractText(raw) {
return normalizeMessages(raw)
.filter((m) => m?.type === "text" && m?.data?.text)
.map((m) => m.data.text)
.join(" ");
}
const out = extractText(raw);
if (typeof out !== "string" || out === "") {
return "OpenDialog returned no text response (turn may be button/form-only)";
}
return out;
The error-handling convention is the same: the body of process is wrapped in
try/catch and any failure is returned as a string rather than thrown, while
the lower-level odPost throws on a non-OK status or non-JSON body and those
throws are caught by process:
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
Adapting it for your own agent
To point this same skeleton at a different service, change:
- The endpoint URL built in
odPost(${apiUrl}/chat-api/message). - The auth header — the
Authorization: Bearerline and any service-specific headers likeOPENDIALOG-SCENARIO-ID/OPENDIALOG-USER-ID. - The request body shape in
buildPayload. - The response field your service uses, by rewriting
extractText/normalizeMessages. - The session handling — the
activeUserId/activeSessionStartedstate and the one-time welcome trigger — to match how your service tracks a conversation. - The secret keys you read from
envand declare in Required secrets.
Set it up
- Copy this integration from the registry — see Registry integrations.
- Create the project secrets above — see Project secrets.
- Preview the agent to confirm it works.