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
| 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
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: 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. - Whether each call starts fresh (single-turn here) or reuses a session.
- 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.