Skip to main content

OpenDialog Single-Turn Agent

This integration calls an OpenDialog Webchat Chat API scenario with a fresh conversation for each input and returns the text response.

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

Single-turn. Each evaluation case starts a fresh conversation. For a conversational variant, use the OpenDialog Multi-Turn Agent.

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.

The entry point is an async process(input) function. input is the user's message text for the current turn:

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();
// ...

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. The input is normalized to a trimmed string. If any secret or the input is missing, the function 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 base URL is also normalized — trailing slashes are stripped and a https:// prefix is added when the value has no scheme:

const normalizeApiUrl = (value) => {
const raw = String(value ?? "")
.trim()
.replace(/\/+$/, "");
if (!raw) {
return "";
}
if (raw.startsWith("http://") || raw.startsWith("https://")) {
return raw;
}
return `https://${raw}`;
};

Because this is the single-turn agent, every call starts a fresh conversation. It generates a random conversation id and derives a per-call user id from it:

const convId = generateConversationId();
const userId = `${OD_TEST_USER}${convId}`;

Each request is a POST to ${apiUrl}/chat-api/message. The OpenDialog routing is carried in headers: a Bearer token in Authorization, plus the OPENDIALOG-SCENARIO-ID and OPENDIALOG-USER-ID headers:

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 body is built by buildPayload, which wraps the message text and the selected scenario inside a content object:

function buildPayload({ scenarioId, userId, type, text, callbackId }) {
const content = {
author: userId,
id: OD_TEST_USER,
type,
user_id: userId,
user: { custom: { selected_scenario: scenarioId } },
data: { text, meta: [] },
};
if (callbackId) {
content.callback_id = callbackId;
content.data.value = callbackId;
}
return { author: userId, user_id: userId, content, notification: "message" };
}

The flow is two requests per call. First a trigger payload with the WELCOME callback id starts the conversation, then the actual text message is sent for the same userId:

await odPost({
apiToken, apiUrl, scenarioId, userId,
payload: buildPayload({
scenarioId, userId, type: "trigger", text: "Welcome", callbackId: "WELCOME",
}),
});

const raw = await odPost({
apiToken, apiUrl, scenarioId, userId,
payload: buildPayload({ scenarioId, userId, type: "text", text }),
});

The response is parsed into a normalized 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 throughout: inside process the whole body is wrapped in try/catch and any failure is returned as a string message rather than thrown out of the function. The lower-level odPost does throw on a non-OK status or non-JSON body, but 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.
  • Whether each call starts fresh (single-turn here) or reuses a session.
  • 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.