Skip to main content

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

SecretWhat it is
OD_API_URLOpenDialog tenant host or base URL, for example your-tenant.cloud.opendialog.ai.
OD_SCENARIO_IDOpenDialog scenario ID to route chat messages to.
OD_API_TOKENBearer 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: Bearer line and any service-specific headers like OPENDIALOG-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 / activeSessionStarted state and the one-time welcome trigger — to match how your service tracks a conversation.
  • The secret keys you read from env and declare in Required secrets.

Set it up

  1. Copy this integration from the registry — see Registry integrations.
  2. Create the project secrets above — see Project secrets.
  3. Preview the agent to confirm it works.