Skip to content

Changelog

This content is not available in your language yet.

Notable API-facing changes, newest first. This tracks what integrators need to know — not every internal change, just anything that adds a capability, fixes a real bug you might have hit, or changes behavior you may be relying on.

  • Docs (clarification): updateWorkflow — spelled out explicitly that nodes/edges are a full replacement at every level, not a field-level merge: omitting a node drops it, and omitting a field (e.g. position) on a node you DO re-send drops just that field. Found live building a partner integration — a re-send that only touched config on some nodes silently wiped position from all of them.
  • Added: The webhook node can now embed a fetched file as base64 into its own bodyTemplate, via a new fetchAsBase64 field — up to 4 entries, each { urlTemplate, bodyField }, fetching either an http(s):// URL or an s3://bucket/key URI and injecting the base64 string at a dot path in the rendered JSON body. Until now this capability only existed on the action node (fetchAsBase64Var) — a webhook node calling a partner endpoint with an embedded file had no way to do so without a separate registered Custom Action. The two fields aren’t identical: webhook’s never touches ctx (it’s injected straight into the request body), while action’s writes the base64 string to a context variable. Also new: action node’s existing fetchAsBase64Var now accepts an s3://bucket/key URI too, not just HTTP(S) — useful when the file already lives in your own S3 bucket rather than being reachable over the public internet. An s3:// fetch uses the platform’s own AWS credentials and is fail-closed: your tenant’s bucket must be explicitly granted first (the platform’s own bucket is always denied, regardless of any grant). Both fetch paths share the same 10 MB cap. See Workflows Overview: fetching files for the full behavior.
  • Added (behavior change worth reading if your graph has a back-edge): A workflow node can now run more than once in a single turn, via a new maxVisits field on the node’s own config — available on every node type, defaulting to 1, capped at 100, validated on draft save and publish. Until now a run visited each node at most once: a back-edge was accepted at publish time and then silently dropped at runtime, with no error, no log line and no trace entry, and the run still reported COMPLETED. If you drew a retry loop and wondered why it never retried, that is why. Set "maxVisits": 3 on the node being retried and the back-edge genuinely re-runs it. Exhaustion is no longer silent, and there are three outcomes: (1) the node has an outgoing edge labeled 'loop_exhausted' — a new reserved label, valid on any node type — and the run follows it, which is how you give up gracefully instead of dying; (2) the node declared maxVisits and has no escape edge, so the run FAILEDs with a named error rather than quietly stopping; (3) the node never declared maxVisits, in which case that branch stops and the rest of the graph carries on exactly as before. All three now write a nodeTrace entry with status: "VISIT_LIMIT_REACHED", visitLimit and visitCount, where previously there was no record of any kind. Nothing changes for a graph that doesn’t opt in — an unset maxVisits reproduces the old behavior precisely, so an accidental back-edge someone drew months ago does not start failing runs today. Three things to check on your side: maxVisits has to be raised on every node inside the cycle, not just the one doing the work — a node left at the default halfway round the loop silently stops that branch before your escape edge is ever reached; nodeTrace may now hold more than one entry for the same nodeId, so anything keyed on “one entry per node” should key on array position instead; and every visit spends one of the run’s 250 node executions, so a loop meeting that ceiling reports BUDGET_EXCEEDED, not VISIT_LIMIT_REACHED. The dashboard’s workflow builder has no input for maxVisits yet — set it in the nodes JSON you pass to updateWorkflow (nodeConfigSchema already returns it for every type if you form-generate your own editor). See Loops and re-entry for a worked bounded-retry graph, and Loop re-entry and VISIT_LIMIT_REACHED for the API shapes.
  • Added: A new workflow node type, await_reply — ask the user a question, end the turn, and continue the same graph from that node’s successor when the next message arrives, with the workflow’s variables intact. Until now every inbound message restarted the graph at start, so “where were we?” had to be re-derived each turn (usually by paying an LLM node to read the history back and guess). Routing is by edge label: answered (required on every await_reply node) and unanswered (required, and only reachable, when matchMode is 'options'); both rules are checked at save/publish time, not mid-conversation. free_text mode takes any reply as the answer; options mode matches it against a list built from a context array and fixed at the moment the question is asked — by slot number, exact id, normalised label, then unique whole-token containment, every tier requiring exactly one winner, so an ambiguous reply cleanly takes unanswered instead of guessing. New surface, all additive: pendingAwaitReplies(sessionId: Int) (reference) lists what is paused and until when; WorkflowRunStatus.AWAITING_INPUT is the paused run’s status (not COMPLETED — exclude it from any “non-completed run = failure” alerting you have); and WorkflowRunDto.resumedFromRunId links the continuation run back to the paused one. Three things to know before adopting it: (1) the question is published with turnComplete: true, because the turn genuinely is over until a human replies — a client using a quiet-period/silence timer instead of turnComplete to decide “the agent is done” should switch before pointing it at a graph that uses this node; (2) a wait expires after 24 hours by default (configurable per node, hard ceiling 7 days) and is then simply dropped — it is never auto-resumed down unanswered, so build your own timeout handling if your flow needs a faster guarantee; (3) the dashboard’s workflow builder has no editor UI for this node yet — author it via updateWorkflow’s nodes JSON (nodeConfigSchema(type: AWAIT_REPLY) already returns a real JSON Schema if you form-generate your own editor).
  • Added (limits you should know about): A workflow run now executes under an explicit budget, and a graph is bounded when it is saved. Runtime: 250 node executions and 10 minutes of wall-clock time per run — exceeding either stops the run, persists it as FAILED, and writes one extra nodeTrace entry with status: "BUDGET_EXCEEDED" plus budgetLimit ("maxNodeExecutions" or "deadline"), budgetNodeExecutions and budgetElapsedMs, attributed to the node the engine refused to dispatch (that node never ran). Save time: 200 nodes / 400 edges max, rejected by updateWorkflow (drafts included) and publishWorkflow with Workflow has N nodes, exceeding the maximum of 200. Both ceilings are far above real usage — the largest graph on the platform is 73 nodes / 76 edges and the longest run ever recorded took 79 seconds — so this should be invisible unless something has genuinely run away. Branch on entry.status === "BUDGET_EXCEEDED" rather than parsing the error text; the three budget* fields appear on that entry only, so existing trace parsing is unaffected. Note the budget is checked between node dispatches, never mid-node, so a single hanging call is still bounded by that node’s own timeoutMs, not by this. See Workflow Overview: Graph size and run limits and Workflows API: Run limits and BUDGET_EXCEEDED.
  • Added: runWorkflowTask — a credentialed, headless way to run an agent’s published workflow as a task, with no chat message, no conversational session to drive and no reply to collect. Auth is X-Api-Key only (no JWT), and the tenant is resolved from the key itself, so the input has no tenant field. Facts travel as a flat variables map plus optional attachmentUrl/attachmentFilename/attachmentMime, seeded into exactly the same run context a chat attachment produces — so one graph can serve both a conversational caller and a task caller. The caller cannot name a workflowId: the agent owns which graph runs, as it does for a chat turn. Intended for an orchestrator’s webhook step, a channel gateway, your own backend, or a cURL. It is asynchronous — a successful return means the run was accepted and enqueued, never that it succeeded; read the real outcome from workflowRuns(sessionId) { nodeTrace context } (note that query is dashboard-JWT only, so an API-key-only integration should have the workflow report its own result outward via a terminal webhook node). Rate-limited at 60/min per client IP — see Rate Limiting: Workflow tasks.
  • Changed: A condition node whose expression references a variable the current run’s context does not hold now evaluates false and takes the false edge. It previously threw, which failed the entire run and produced a generic “Sorry, I ran into a problem” reply for what is usually just a turn that didn’t carry that fact. The unresolved names are recorded on the run’s context as _conditionUnresolvedVariables, so the reason is visible from workflowRuns(sessionId) { context }. Deliberately narrow: only an unresolved-variable reference maps to false — a syntax or type error still throws. If you were relying on a missing variable hard-failing a run, add an explicit gate instead. Practical upshot: someId > 0 is now a complete guard covering absent, 0, "0", "" and prose in one expression — use it (never someId != 0, which does not coerce) in front of any node that writes outward.
  • Added: A second, per-tenant rate limit now layers on top of the existing per-client-IP limit on every avatar-service route — POST /tts, POST /tts/google, POST /stt, and the newly-documented POST /avatar-session (see below). Both limits must pass — a caller can’t exceed either one. Per-tenant ceilings are set to 20x the existing per-IP figure on each route (600/min for TTS routes, 200/min for STT and avatar-session). An unauthenticated/invalid-token request is governed by the per-IP limit alone; the tenant limit only applies once a valid avatarToken resolves a tenant. See Rate Limiting: Avatar service and the REST reference.
  • Docs (previously undocumented): POST /avatar-session — negotiates a HOSTED_API (photorealistic video avatar) rendering session. This endpoint already existed but had never been written up on this site.
  • Docs (correction): Avatar Service REST API: POST /tts/google said its rate limit was “shared with /tts.” It isn’t — /tts and /tts/google each have their own independent counter (@nestjs/throttler keys per route handler); they just happen to use the same numeric limit. Corrected on that page.
  • Added: Knowledge base document upload (requestKnowledgeDocumentUpload) now accepts Markdown (text/markdown), CSV (text/csv), and XLSX (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet), alongside the existing PDF and DOCX — same 20MB cap, same three-step upload flow. Markdown is passed through as-is (no stripping); CSV and XLSX are re-serialized row-by-row as Header: value | Header: value | ... lines (XLSX flattens every worksheet, each demarcated by a === Sheet: <name> === line) rather than dumped as flat comma-joined text, specifically so a chunk boundary landing mid-file doesn’t strand a value with no way to tell which column it came from. See Knowledge Base (RAG): File upload.
  • Added: New action node config field, extractTextVar — an alternative output mode for fetchAsBase64Var’s fetch: instead of base64-encoding the fetched bytes into ctx[outputVar], it extracts plain text from them (through the same extractor registry the knowledge-base upload pipeline above uses — PDF, DOCX, Markdown, CSV, or XLSX) and writes the result into ctx[extractTextVar] instead, with ctx[outputVar] left unpopulated. Capped at extractTextMaxChars (default 50,000 characters, hard ceiling 200,000); ctx["${extractTextVar}Truncated"] is always written as a boolean so your graph can branch on a partial extraction. Requires fetchAsBase64Var to also be set. For mid-conversation document reading (a partner API hands back a file URL you want an llm node to read), as distinct from Knowledge Base ingestion, which is for content you want retrievable across future sessions.
  • Added: New llm node config field, imageVars — attach up to 4 images (named context variables holding a data URL or bare base64 string, ~5MB cap each) to a model call, alongside promptTemplate’s text. Read this before using it: the platform’s default LLM provider does not support image input, and an imageVars-bearing turn on an incapable provider fails the entire workflow run, not just the node — llm nodes have no success/failure edge routing to fall back on the way tool/action nodes do. Set modelOverride, or an agent-level provider override, to a vision-capable provider before using this field.
  • Docs: <vai-avatar>: Installation now documents a real gotcha for any integration that manages the SDK’s <script> tag itself (an SPA re-mounting the embed on route change, or a custom loader): removing and re-injecting vai-avatar.js in the same page session throws SyntaxError: Identifier '...' has already been declared, because its top-level const declarations persist in the browser’s global scope even after the <script> element is removed from the DOM. Fixed in Wetel’s own dashboard; if you wrote your own embed-loading logic, keep the script tag in the DOM permanently and only add/remove the <vai-avatar> element itself.
  • Docs (new page): Image Understanding for an Agent ties together the marker/fetchAsBase64Var/imageVars pieces documented individually above into one end-to-end walkthrough — how an attached image reaches a workflow, the graph shape that gets it into a model call, and the vision-capable-provider requirement (llmProviderOverride) it depends on. Includes a concrete right-vs-wrong example of the attachment marker’s leading-block-only parsing rule (a marker followed by text on the same line silently fails to parse — this bit a real integration the same day this shipped).
  • Added: POST /stt now accepts OGG_OPUS as an audioEncoding value, alongside the existing MP3/LINEAR16/WEBM_OPUS. This matters because OGG_OPUS is the native format WhatsApp and Telegram voice notes are already recorded in — if your integration forwards a voice note to /stt, you no longer need to transcode it first. Defaults to a 16kHz sample rate when sampleRateHertz is omitted (matching WhatsApp’s native rate), distinct from WEBM_OPUS’s 48kHz default. New walkthrough: Sending Voice Messages to an Agent — a copy-paste guide for transcribing a voice note with /stt and feeding it into an agent via sdkSendMessage, using the avatarToken from sdkStart (nothing restricts that token to the <vai-avatar> widget — it works the same way called directly from your own backend).
  • Docs (correction): Avatar Service REST API: POST /stt overstated the request size limit as “~5MB of raw audio (~7,000,000 base64 characters)”. The real, current limit is ~4MB of raw audio (~5,600,000 base64 characters) — corrected on that page.
  • Added: refreshAvatarToken — renew a session’s avatarToken without starting a new session. An avatarToken lives about an hour, and until now there was no way to renew one: a conversation that outlived its token simply started failing with Invalid or expired avatar token, with no recovery path short of a fresh sdkStart (and a fresh session). This matters for any long-running session — an embed left open on a tab, a channel-bridged chat a user returns to later, a headless integration holding one session for hours. Pass the current token as input.avatarToken (not as an Authorization: Bearer header — this is the one avatarToken operation that has to accept an already-expired token, which the normal header guard rejects outright); you get back a fresh full-TTL token, the same sessionId, and expiresInMs so you can schedule the next refresh off the server’s TTL instead of hardcoding an hour. You don’t need to refresh pre-emptively: a token that expired within the last 10 minutes is still accepted (AVATAR_TOKEN_REFRESH_GRACE_MS, configurable per deployment), so the reactive “call → get the auth error → refresh → retry” pattern works. Only the expiry bound is relaxed — the signature is verified in full, the session must still be ACTIVE, and the replacement carries the same session, tenant and avatar config, so it never widens a token’s authority. Rate-limited to 10/min per client IP (see Rate Limiting). The one thing to get right: a refreshed token cannot be swapped into a live sessionEvents WebSocket — graphql-ws reads connectionParams only once, at connection open — so close and reopen the subscription with the new token. That failure is silent, not loud: HTTP mutations resume on the new token while an un-reconnected socket holds an expired one and quietly stops delivering events. sdkSendMessage/sdkEndSession need no special handling; each sends its own header and picks the new token up on the next call. Full walkthrough in Authentication → Refreshing the session token.
  • Docs (correction): Authentication said an avatarToken was “short-lived (about 24 hours)”. It has been about an hour (AVATAR_TOKEN_TTL_MS, 3,600,000 ms, on every deployed environment). If you sized a session-keepalive or refresh strategy off the 24-hour figure, it was wrong by a factor of 24 — this is exactly the gap refreshAvatarToken above now closes.
  • Added: credentialSecretHeaderName on createCustomAction/updateCustomAction. A Custom Action’s stored bearerToken used to only ever render as Authorization: Bearer <token> — this lets you send it under any static header name instead (e.g. X-Api-Key, X-Client-Secret), with no Bearer prefix and no Authorization header sent, for third-party APIs that authenticate that way. Only applies to the plain self-service bearerToken credential; rejected on create unless bearerToken is supplied in the same call, and rejected on update if the action has no stored secret or uses a Wetel-provisioned/session-identity credential source. See Action Node: Credential header name for how it interacts with extraHeaders.
  • Changed: extractFirstInteger now stores a real number in outputVar, not a digit string — it used to be a string. Motivation: a condition node’s expression is evaluated with no type coercion, so a "0" string sentinel compared with myVar != 0 evaluated true (a string is never equal to a number), silently routing the “no match” case down the found branch. A live run hit exactly this and fired two outbound writes with an id of 0 before the partner API rejected them. If your workflow gates on this sentinel with myVar != "0" or {{#if myVar}}, update it to a plain myVar != 0 / myVar > 0 — that’s now what the value actually is. Rendering in a template is unaffected: {{myVar}} and "{{myVar}}" still render the same way they did before.
  • Changed: the findId helper’s idField argument can now be a dotted path (e.g. "current_application.id") to reach a nested field. A missing or non-object intermediate segment resolves to the same 0 “no match” sentinel rather than throwing, so a partner-side shape change degrades onto the existing not-found branch instead of failing the run. A numeric-string id (some partner APIs, including Nortia, return ids as "21" rather than 21) is now coerced to a real number too — findId always returns a number or 0. Motivation: a nested current_application.id lookup that a flat-field-only findId couldn’t reach at all.
  • Changed: the findId Handlebars helper now matches in three tiers — exact, then normalised (case, whitespace, punctuation, diacritics, trailing plurals), then unique whole-token containment — and returns 0 whenever a tier is ambiguous instead of picking the first hit. Motivation: a live run where an llm node wrote Solutions Architect for a posting titled Solution Architect; the exact-only lookup rendered an empty id and the partner API rejected the write with a 422 after the agent had already told the candidate it was filing their application. Behaviour change to know: duplicate values in the array now return 0 at every tier (previously first-wins). Details and the “route the 0 case before any write” rule in Workflow Best Practices.
  • Added: Channel attachment/marker parsing moved server-side. Every workflow run’s context now carries attachment ({ url, mime, filename } or null), channel ({ trigger, nortiaEnabled, nortiaCompanyId, raw }), and flat attachmentUrl/attachmentMime/attachmentFilename mirrors (empty string when absent) — seeded from the leading block of [[...]] marker lines a channel prepends to an incoming message (the Telegram gateway’s [[WETEL_ATTACHMENT url=... mime=... filename=...]], and OmniChat’s one-per-line [[trigger:...]]/[[nortia:enabled|company|resumeUrl|resumeName=...]]). If your graph used to run its own llm node to pull url/mime back out of the marker text, you can delete it — use the flat mirrors directly in a condition expression (they’re safe to compare even with no attachment present) and as fetchAsBase64Var’s source (it takes a bare variable name, not a dot path). Markers are not stripped from userMessage, so a graph that already extracts them itself keeps working unchanged. See Channels: attachment markers and the updated file-intake cookbook. If you’re bridging your own channel (not a packaged Channel Connector) into sdkSendMessage, neutralize [[ in any end-user-typed text before prepending your own marker — otherwise a user-typed line can be parsed as if the platform itself sent it.
  • Resolved (follow-up to 2026-09-09’s open question below): Nortia’s REST Integration API (List Job Postings (REST), Submit Resume (REST)) now authenticates with a static, service-owned secret header — Nortia issued Wetel one, and the old bearer-token path these two operations previously relied on has been removed on Nortia’s side entirely. Both operations are confirmed working end-to-end against the real API. Behavior change if you had a Custom Action pointed at either operation with credentialSource: "SESSION_NORTIA": that combination is no longer offered — a workspace-wide service secret carries no signed-in end user’s identity, so a per-user credential source no longer makes sense for these two operations specifically. Use credentialSource: "NORTIA" (Wetel-provisioned shared credential) instead; SESSION_NORTIA is unaffected on every Nortia GraphQL operation, where it continues to carry the real signed-in user’s own identity as before.
  • Docs (correction): Rate Limiting was stale — it still listed sdkStart as unthrottled (it has been 30/min since 2026-09-08) and omitted generateWorkflowDiff (20/min), ingestDocumentFromUrl, generateText/startTextGeneration, and the auth-mutation limits added 2026-09-08. The page now lists every enforced limit, and states explicitly that all GraphQL limits are keyed by client IP, not by API key or tenant — two other pages that said “per key”/“per tenant” were corrected to match.
  • Docs (previously undocumented): nodeConfigSchema / nodeConfigSchemas — shipped 2026-08-31, callable with a JWT or an API key, returns a real JSON Schema per workflow node type so an OEM builder can form-generate its property panels from the live schema instead of a hand-copied snapshot. Build Your Own Admin Panel now points at it, and its per-node config table was corrected (llm should expose promptTemplate, not systemPrompt, as the main field; action is a Custom Action + certified-operation picker, not a raw URL form; end_session has no final-message field).
  • Docs: sub_agent now documents two requirements previously only discoverable at runtime — every target agent must have a published workflow (otherwise that target returns status: 'error'), and the dispatch depth cap is exactly 5.
  • Docs: Workflow Best Practices documents the third Handlebars helper, joinList, and states plainly that there is no eq/comparison helper — equality branching belongs in a router/condition node.
  • Fixed (read this if a workflow using a Nortia certified operation is reading current_status, reply, or dept_head_name from a response): Nortia’s live API changed a handful of response field names since these operations were first certified — 10 of Wetel’s certified Nortia operations broke as a result (they would have failed outright at runtime). All 10 are fixed as of today: Application.current_status is now status (affects Update Application Status, Move Application Stage, Record Not Hired, Record Manager Decision, Record Interview Outcome, Propose Expert Interview, and Assign Candidate to Job); AgentAnswer.reply (Ask Matchie) is now answer; Department.dept_head_name (Update Department) is now a relation, dept_head { id name }, rather than a plain string field; and JobPosting.interview_strategy (Get Job Posting) was removed with no confirmed successor yet, so that field is temporarily dropped from the certified selection rather than guessed at. Every affected operation’s certifiedOperationId is unchanged — the fix was applied to the operation’s own definition, so no workflow needs re-wiring. If your workflow templates read one of the old field names from a Nortia action’s output, update the template to the new name.
  • Added: 4 new Nortia certified operations — Get Candidate (candidate(id), read), Get Interview Info (interview(token), read — requires the interview invitation token, not a general “my interviews” list), AI Usage (aiUsage, read), and Update Job Posting (updateJobPosting, write — takes a full replacement payload, not a sparse patch). Catalog total is now 176 (Nortia 79 — 77 GraphQL + 2 REST, OmniChat 59, aisCRM 38).
  • Documented (no behavior change yet — open question): Nortia’s REST Integration API documentation now describes a different authentication scheme (a static, service-owned secret header) than the bearer-token flow Wetel’s REST certified operations (List Job Postings (REST), Submit Resume (REST)) currently use. Whether the existing bearer-token flow still works has not yet been confirmed either way — nothing has changed on Wetel’s side pending that confirmation. If this turns out to be a breaking change, it will get its own callout here.
  • Fixed (security — read this if any ACTION/WEBHOOK/TOOL/MCP target or externalSyncUrl ever redirects, or its hostname doesn’t resolve): Outbound calls made on your behalf — action nodes, webhook nodes, tool/MCP connector calls, ingestDocumentFromUrl, and externalSyncUrl — no longer follow HTTP redirects blindly. A redirect target is now either rejected outright (action/webhook, which set maxRedirects: 0, since no legitimate target needs to redirect) or re-validated against the same private-address check before being followed (the fetch-based paths: knowledge-document ingestion, MCP connector calls, externalSyncUrl). Separately, a target hostname that doesn’t resolve at all is now rejected, both at save time (updateAgent, workflow publish) and at call time — previously an unresolvable hostname was treated as allowed rather than blocked. If your action/webhook URL legitimately 301/302/307/308s (e.g. an http://https:// upgrade, a trailing-slash normalization), point the node directly at the final URL — see Troubleshooting for the exact symptom. If a URL’s hostname doesn’t resolve from Wetel’s own servers, it can no longer be saved at all (e.g. an internal-DNS-only name, or a partner endpoint mid-provisioning) — the error names the hostname so it’s diagnosable.
  • Changed: On a Custom Action whose credentialSource is a Wetel-provisioned, tenant-wide shared credential ("OMNICHAT" / "NORTIA" / "AISCRM" / "LARK_TENANT"), url and method are now immutable via updateCustomAction — a change to either is rejected with an error telling you to create a new action instead. This closes a gap where such an action could be repointed at an arbitrary endpoint while still sending Wetel’s own shared partner credential on every call. Session-identity credential sources (SESSION_OMNICHAT/SESSION_AISCRM/SESSION_NORTIA — your own end user’s identity, not a Wetel-shared one) are unaffected; you can still retarget those the same as before.
  • Changed (no client change needed for any existing integration): The avatarToken returned by sdkStart is now bound to the specific session it was issued for. sdkSendMessage, sdkEndSession, sdkVerifyWebbyxOneIdentity, requestStudyKitExport, interruptSession, and the sessionEvents subscription now reject any call whose sessionId doesn’t match the sessionId the presented avatarToken was actually minted for, with Forbidden. Every integration that already uses the {sessionId, avatarToken} pair together exactly as returned from a single sdkStart call — which is every known caller in this repo’s own reference tools and SDKs — is unaffected. Tokens minted shortly before this change keep authenticating during a short rollout window via a legacy compatibility path; that path is temporary and will be removed in a follow-up.
  • Added: Rate limits now apply to login, register, refreshToken, loginWithWebbyxOne, isRegisterable, generateOTP, validateSecuredToken, forgotPasswordWithOTP, resetUserPasswordWithUrl, and sdkStart. A caller hammering one of these repeatedly now gets a rate-limit error instead of an unlimited retry budget — keyed per source IP, so a shared IP in front of many end users (a corporate NAT, a proxy) shares one budget across all of them.
  • Breaking: generateOTP no longer accepts the optional top-level audience argument — it has been removed from the schema entirely. If your integration ever passed audience alongside input (even the default "customer" value), drop it; the call now only takes input. No known caller passed a non-default value; if yours did and it appeared to work, that was actually a gap — a non-"customer" value let an unauthenticated caller mint an OTP for a contact that had never registered.
  • Changed: OTP codes generated by generateOTP are now purpose-bound — a code minted for one TokenPurpose (e.g. SIGN_UP) can no longer be spent against a different flow expecting a different purpose (e.g. forgotPasswordWithOTP’s RESET_PASSWORD). Each code also now locks out after 5 wrong attempts, returning the same generic error used for an unknown or expired code — so a caller can’t distinguish “wrong code,” “locked out,” and “no such code” from the response alone. If your integration reuses one OTP across more than one purpose, or retries a wrong code more than a few times, both now fail; request a fresh OTP for the specific purpose you need.
  • Added: response node structuredContentTemplate and the corresponding MessageDto.structuredContent field. A response node can now attach an optional structured payload (card | table | list) alongside its plain-text reply. Rendering fails safe — a bad template never fails the node, structuredContent is just null and the text reply still sends. Dashboard-only consumer today; no interactive/button variant yet.
  • Docs: New recipe, Channel Agent with File Intake and a Partner Write-Back — a generalized, replicable build guide for a headless channel-connected agent that collects information over a conversation, accepts a file, and writes the result to a partner API via a certified operation. Generalized from a real, Kai-verified-end-to-end Telegram + Nortia HR screening build (role selection, a live PDF résumé submission, and a real candidate record created on Nortia). Includes a copy-pasteable workflow graph and a gotchas checklist covering the reliability patterns this build actually hit.
  • Documented (not a behavior change): Channels now documents the [[WETEL_ATTACHMENT url="..." mime="..." filename="..."]] marker a channel connector prepends to an incoming message’s text when the user sends a file — this convention already existed (shipped alongside Channel Connectors’ Telegram document/photo intake) but was never written up publicly until now. Also newly documented: Telegram’s /start session reset — ending the mapped session and replying with the agent’s own openingGreeting on /start, giving an end user a self-serve way out of a long or stuck conversation with no operator intervention needed. Both behaviors are already live; this closes a real documentation gap, not a new capability.
  • Docs: Workflow Best Practices now documents the findId Handlebars helper in full (signature, worked example, the specific “an LLM confuses a code/ISBN’s own digits for the real id” failure it exists to avoid). Action Node and Tool Node already linked here for this, but the section itself didn’t exist yet — those links previously landed on a page with no matching content.
  • Added: runRegressionScenario and workflowRegressionResults — Phase B of workflow regression scenarios. A saved scenario (captureRegressionScenario, shipped 2026-09-01) can now actually be replayed against your workflow’s current draft, and gets a real pass/fail verdict. Pass/fail is structural only — the same node-visitation path, router/condition branch, tool calls, and terminal node as the baseline — never the literal wording of an LLM/voice response, since that’s non-deterministic by design. runRegressionScenario returns immediately (before the replay finishes, since it drives a real LLM turn); poll workflowRegressionScenarios/workflowRegressionResults a few seconds later for the outcome.
  • Added: agentAuditLogs — this agent’s change history (who changed what, when). v1 covers updateAgent only — a field set to the value it already had is not recorded, and there’s no tenant-wide admin log across every agent yet, just this per-agent history.
  • Added: setPrimaryBillingAccount — sets which system bills your tenant’s usage: WEBBYX_ONE (WebbyX One’s own credit ledger) or WETEL (Wetel’s own billing; re-enables the tenant’s normal per-plan monthly LLM spend cap). Explicit and user-set — never inferred from which login method a given session happened to use. Selecting WEBBYX_ONE requires the account already have a linked WebbyX One identity via linkIdentity — throws otherwise. myTenant now exposes the current value as primaryBillingAccount.
  • Added: the certified operation catalog is no longer exclusively GraphQL — 2 new Nortia entries, List Job Postings (REST) and Submit Resume (REST), run against a separate plain REST API (api-recruitment.webbyx.dev’s “Integration API,” distinct from Nortia’s GraphQL admin API). New operationType values "rest_get"/"rest_post" alongside the existing "query"/"mutation". For a REST certified operation, argsTemplate is the full request body/params directly (no {query, variables} wrapper) — see Action Node: Certified operations. Both new entries are schema-verified only — read directly from Nortia’s own live, published OpenAPI spec — not yet live-execution-verified; see the operations’ own verificationNote. Catalog total is now 172 (Nortia 75, OmniChat 59, aisCRM 38).
  • Added: a new action node config field, fetchAsBase64Var — an alternative to the normal customActionId-driven call. Set it to a context variable holding a URL, and the node fetches that URL’s bytes server-side, base64-encodes them, and writes the result to ctx[outputVar]. Built specifically to feed a certified operation’s file_base64-shaped request field (e.g. the new Submit Resume (REST) above) without needing a real GraphQL-multipart file upload, which no executor in this platform supports today. customActionId becomes optional on action nodes when fetchAsBase64Var is set — every other field on the node is ignored in this mode. Same SSRF protection as every other action node call; sends no Authorization header, so only point it at a URL meant to be fetched without auth.
  • Added: Request limits — every GraphQL query/mutation now enforces a max query depth of 7 and max query complexity of 50, rejected as a pre-execution validation error. Applies in all environments, doesn’t affect introspection. The schema’s own real nesting is shallow (one Relay connection, no deep object graphs), so this shouldn’t affect any normal integration query — it’s a worst-case resource-cost bound, not a new constraint on legitimate usage.
  • Fixed: on a tool/action/webhook node’s HTTP failure, nodeTrace’s error field now includes the partner endpoint’s own response body — its status code and JSON message/error/errors field (or up to ~500 chars of raw text for a non-JSON body) — instead of only axios’s generic "Request failed with status code 400". Previously the actual reason a partner rejected a request (a validation message, a missing-field error) was invisible from Wetel’s side entirely; this was the exact blind spot behind an unresolved aisCRM write-400 in our own integration testing. Bounded and redacted the same way as renderedRequest. Not retroactive.
  • Changed: sdkSendMessage’s and sendMessage’s text field limit raised to 10,000 characters (previously 4,000 for sdkSendMessage, 5,000 for sendMessage — the two had drifted with no principled reason for the difference; both are aligned now). No other behavior changes — the field was already stored in an unbounded Postgres column, this is purely the request-validation ceiling. If your integration constructs a large per-turn payload (e.g. embedding conversation history or system instructions directly into text), consider whether Wetel’s own Agent personaPrompt and native multi-turn history would let you send a shorter message instead — cheaper on LLM input tokens per turn, and won’t hit this ceiling again as conversations grow.
  • Expanded: the certified operation catalog’s Nortia coverage grew from 64 to 73 operations, adding 9 hiring-pipeline write mutations: application status/stage updates, application notes, not-hired/manager-decision/interview-outcome recording, expert-interview proposal, candidate assignment, and candidate contact-detail updates. All SESSION_NORTIA-compatible. Unlike most of the catalog, these 9 are schema-verified only, not live-execution-verified — Nortia has no disposable candidate/application fixture to safely round-trip a write against, so exercising them for real would mutate an actual hiring record. Check an operation’s verificationNote before relying on one for anything beyond a controlled test. Catalog total is now 170 (Nortia 73, OmniChat 59, aisCRM 38).
  • Fixed: createCustomAction/updateCustomAction now accept an input.credentialSource field, restricted to "SESSION_OMNICHAT"/"SESSION_AISCRM". Previously these two values were never actually reachable through either mutation — despite Credential sources already describing SESSION_OMNICHAT/SESSION_AISCRM as “any tenant, self-serve,” there was no way to set a new custom action onto that flow without direct database/seeder access, so the practical self-serve path for calling OmniChat/aisCRM described on this page and on the certified operation catalog page didn’t fully work end to end until now. Mutually exclusive with bearerToken/larkAppId+larkAppSecret; switching an existing action onto a SESSION_* source clears any stale stored credential. OMNICHAT/NORTIA/AISCRM (tenant-wide shared account) and LARK_TENANT remain not directly settable here — see the credential sources table for why.
  • Added: createCustomAction/updateCustomAction’s input.credentialSource now also accepts "SESSION_NORTIA", alongside the existing "SESSION_OMNICHAT"/"SESSION_AISCRM". Nortia now has the same self-serve, session-identity-driven credential flow as OmniChat and aisCRM — sign in via WebbyX One, link your Nortia identity (sdkVerifyWebbyxOneIdentity’s productTickets), then reference the action’s id. No Wetel-provisioned shared account needed. See Credential sources and the certified operation catalog for the full picture.
  • Added: renderedRequest (see below) now runs through a best-effort secret-redaction pass — JWTs, Bearer tokens, vendor key prefixes (sk-, pk-, Slack, AWS), and long values under sensitive-sounding key names are redacted before the trace is stored. Defense-in-depth, not a guarantee — see Observability & Debugging Your Agent for what it does and doesn’t catch. Never hardcode a secret into argsTemplate/bodyTemplate — use a Custom Action’s credential-source mechanism instead.
  • Added: workflowRuns/workflowRun’s nodeTrace entries now include renderedRequest for tool, action, and webhook nodes — the actual interpolated request body/args sent to the partner endpoint, captured on both success and failure. Previously only the response (outputValue/error) was visible in the trace, making it hard to tell whether a bad result came from a malformed argsTemplate/bodyTemplate on Wetel’s side or an issue on the partner’s own endpoint. Usually an object, but comes through as a raw string if the template failed to render to valid JSON; absent on trace data captured before this shipped. See Observability & Debugging Your Agent.
  • Added: Login with WebbyX One — three new mutations let a person log into the Wetel dashboard using their WebbyX One (IAM One) account: loginWithWebbyxOne(ticket) (public, logs in or silently creates a Wetel account + auto-provisions/joins a tenant keyed to the WebbyX One company), and linkIdentity/unlinkIdentity (JWT-guarded, add/remove a WebbyX One link from an already-logged-in account’s settings). Purely additive — never replaces or disables password login for any account; if a WebbyX One identity’s email matches an existing Wetel account, it auto-links to that account and its existing tenant always wins over a company-based one. This is a distinct mechanism from sdkVerifyWebbyxOneIdentity (session-scoped end-user identity linking for calling certified operations) — see the note on both pages if you’re unsure which one you need.
  • Added: Opt-in success/failure edges for tool and action nodes — draw one outgoing edge labeled success and one labeled failure (same mechanism condition nodes already use for true/false) and the runner routes to failure on any thrown error instead of failing the whole run, writing the error message to a _lastNodeError context variable. Fully additive and backward compatible — a node with neither label keeps today’s exact behavior (every edge fires unconditionally, an unhandled error still fails the run). A failure edge requires a matching success edge; publishing without one is rejected. Available in the Workflow Editor UI via a card-toolbar toggle that swaps the single generic handle for the labeled pair.
  • Added (Phase A — capture and list only; replay shipped 2026-09-06, see above): Workflow regression scenarioscaptureRegressionScenario freezes an already-completed WorkflowRun as a named baseline (reconstructed input script, nodeTrace snapshot, and derived structural assertions), and workflowRegressionScenarios lists them per workflow. This exists to catch a real, documented failure mode — an intent/classification LLM node silently misrouting after a model change, with no error anywhere.
  • Expanded: the certified operation catalog (launched 2026-08-29, below, with 3 starter operations) grew to 161 certified operations — Nortia (64), OmniChat (59), aisCRM (38) — covering near-complete read and write access to each partner’s real API, not just a handful of example queries. New coverage includes full CRUD on OmniChat contacts/conversations/tasks/teams/opportunities plus automation/macro/SLA/canned-response management; Nortia candidate and job-posting lifecycle operations, talent pools, company settings, and usage/billing reads; and aisCRM companies/contacts/deals (including pipeline and win/loss moves), tasks, notes, and pipeline configuration. Every entry was verified live against each partner’s real, current API before being added — check certifiedOperations(product: ...) for the authoritative, current list.
  • Fixed: the certified operation catalog (added 2026-08-29, below) returned an empty list / a 404 on certifiedOperation(id) against both staging and production for a short window after launch — the table existed but hadn’t been seeded yet. Fixed same day; certifiedOperations/certifiedOperation are confirmed live and returning real data on both aws-staging and aws-production now. If you hit this earlier, it’s resolved — try again with the id your own certifiedOperations query returns (ids are per-environment; don’t hardcode one from a doc example).
  • Added: the certified operation catalog — for OmniChat, Nortia, and aisCRM action nodes, pick a Wetel-verified GraphQL query or mutation from certifiedOperations/certifiedOperation instead of hand-authoring the request body yourself. Set ActionNodeConfig.certifiedOperationId and argsTemplate becomes that operation’s GraphQL variables. Also available from the Workflow Editor UI as a second dropdown on the action node’s config panel, once the chosen Custom Action’s credential source has a matching certified catalog. Fully additive — existing hand-authored argsTemplate bodies keep working unchanged. See Custom Actions API: Credential sources for which credential sources are self-serve today (SESSION_OMNICHAT/SESSION_AISCRM via WebbyX One sign-in) versus Wetel-provisioned only (OMNICHAT/NORTIA/AISCRM).

  • Added: a new sub_agent workflow node type — dispatches to one or more other agents concurrently, each running as its own independent session (own turn history, own cost tracking), and aggregates whatever comes back into ctx[resultVar]. This is the building block for multi-specialist orchestration: a dispatcher agent classifying a message and handing off to a billing specialist and a technical specialist at the same time, then combining both replies. One target failing or timing out never blocks or fails the others — see Sub-Agent Node for the full config reference, including the important gotcha that resultVar is always an array, even for a single target, so {{resultVar}} on its own renders [object Object] — index a specific entry ({{resultVar.[0].text}}) instead.

  • Docs fix (read this if any llm workflow node sets both systemPrompt and promptTemplate): LLM Node’s systemPrompt field description was wrong, and this page’s own worked example was broken as a result — copying it produced a node that silently never receives its promptTemplate data. When systemPrompt is set, the rendered promptTemplate is used only for token-cost accounting and never reaches the model in any form, not “used as well” as the old description said. The model still answers — with zero real context — which for a data-lookup node means a confident, fabricated-sounding reply with no error anywhere. Fix: leave systemPrompt unset in almost every case — put your instructions and any {{ctxVar}} data injection together in promptTemplate alone. The page’s worked example is corrected to match.
  • Added: createCustomAction now supports Using Lark — send messages, create/query Base records, route approvals, and more, using your OWN Lark app (not a shared credential). Supply larkAppId/larkAppSecret/larkRegion instead of bearerToken (mutually exclusive with it); Wetel exchanges them for a Lark tenant_access_token and keeps it refreshed automatically. No new node type — this is a Custom Action/ACTION node integration like any other.
  • Added: New LLM API page — generateText (single-response) and startTextGeneration + textGenerationEvents (streaming) let you call an LLM directly through Wetel with no session, agent, or workflow, and no need to set up your own model provider credentials. API-key auth, same tier as embed. textGenerationEvents is the first subscription in this API to authenticate via X-Api-Key in connectionParams instead of a dashboard JWT — see the page’s own WebSocket auth note if you’re building a streaming client.
  • Added: Both LLM API operations now enforce a per-tenant monthly spend cap by plan tier. A call made once you’re at or over the cap fails immediately with a BadRequestException naming your limit and current spend — before any provider is called, so it never incurs cost.
  • Docs: LLM API gained a Troubleshooting section after live-testing the streaming path end to end — Redis pub/sub delivers textGenerationEvents with no replay buffer, so a client that subscribes even slightly after generation finishes silently receives nothing (no error either side). Documents the fix (connect, wait for the socket to actually open, then start generation) and the graphql-ws gotcha behind it: the client’s default lazy: true means connected never fires until something calls subscribe() first, which deadlocks a naive “wait for connected” pattern — pass lazy: false instead.
  • Fixed (behavior change — read this if you edit a published workflow via the API): Editing an already-published workflow via updateWorkflow no longer takes effect on a live session automatically. Previously, updateWorkflow wrote directly onto the same graph a live session executed, so an edit to an already-published workflow went live the instant the mutation returned — with no republish step, contrary to what this page’s own “two-call create pattern” section already documented for a first-time publish. That was an unintended gap, not documented behavior. Now updateWorkflow always writes to a separate draft; a live session only ever runs whatever was captured by the most recent publishWorkflow call. If your integration edits an already-published workflow and expects the change live without a follow-up publishWorkflow call, add that call now — see Workflows API: the two-call create pattern for the corrected, now-accurate-in-all-cases description. The workflow query’s own nodes/edges fields were also clarified to state plainly that they reflect the draft, not necessarily what’s live.
  • Added: sendMessage accepts a new testDraft: Boolean input field — when true, that turn runs the agent’s workflow’s live draft instead of its published snapshot, no publish required. For testing in-progress edits from a dedicated test session only; never set this on a real user’s turn.
  • Added: Five new knowledge-base operations — deleteKnowledgeDocument (soft-deletes a single document and its chunks, unlike deleteKnowledgeBase, and releases the tenant’s quota reservation), resyncKnowledgeDocument (retries a FAILED file’s parse, or re-chunks/re-embeds a READY document’s stored text in place), ingestDocumentFromUrl (ingests a single web page — no link-following, SSRF-protected), knowledgeDocument (fetches one document including its full rawText, kept separate from the knowledgeDocuments list query so listing a KB never pays for fetching every document’s text), and updateKnowledgeDocumentText (overwrites a document’s text and re-indexes it in place — the “edit” counterpart to resync). All fully additive; no existing operation’s behavior changed.
  • Added: New query apiUsageSummary — this month’s LLM spend, the tenant’s monthly spend cap, and a per-model/per-operation call-count breakdown. Backs the dashboard’s new Developers page. Tenant-wide only; today’s schema can’t break usage down by individual named API key.
  • Added: sdkVerifyWebbyxOneIdentity now accepts an optional productTickets array and returns a new linkedProducts result field — a session’s signed-in WebbyX One identity can now be linked to sibling WebbyX-group products (aisCRM, OmniChat today) as itself, instead of a workflow always authenticating against those products as a single shared, admin-configured service account. Fully additive: omit productTickets and nothing changes. Requires minting an extra ticket per product client-side, in the same sign-in submit as the main ticket — see the mutation’s docs for why this can’t be done later or server-side.
  • Added: New query webbyxOneProductClientIds — returns the public WebbyX One X-Client-Id for every sibling product a signed-in identity can link via the productTickets field above. Lets a sign-in widget discover which extra /sso/login calls to make without hardcoding a product list. Accepts either a dashboard JWT or the avatarToken from sdkStart.
  • Added: Custom Actions now support extraHeaders on createCustomAction/updateCustomAction, returned on CustomActionDto — a plain { [key: string]: string } map of additional static headers merged onto every call made by an ACTION workflow node, alongside (and never overriding) the existing Authorization header. Unblocks target APIs that require more than one static header, such as a partner requiring both Authorization and a separate x-organization-id identifier header. Not a secret-storage mechanism — it’s stored and returned as plain JSON, visible to anyone who can query customActions for your tenant; genuinely secret values still belong in bearerToken, the one credential a custom action supports. See Custom Actions API: Extra static headers and Action Node.
  • Docs: Workflow Best Practices now documents the _{outputVar}Structured / _{outputVar}StructuredJson context keys a tool node writes alongside its plain-text outputVar — populated from the MCP protocol’s structuredContent field, or auto-parsed as a fallback if your server returns JSON-shaped text instead. Also documents two supported response node design choices for consuming it: extracting just a human-readable field for a plain-text client, or hand-authoring a structured JSON envelope (with jsonString) for your own frontend to parse client-side. Neither behavior is new — this closes a real documentation gap found while helping a partner debug a raw-JSON-leaking reply, which turned out to be a template-wiring choice, not a bug in either the platform or their integration.
  • Fixed: sdkVerifyWebbyxOneIdentity’s docs incorrectly described the SSO login step as a redirect to a WebbyX-One-hosted login page, and stated the ticket TTL as ~60 seconds. Neither is correct: WebbyX One has no hosted login page for this flow — your own frontend hosts the email/password form and calls POST /sso/login directly with those credentials — and the real, confirmed ticket TTL is 5 minutes. Docs corrected to match the real, live-tested contract; the mutation’s own behavior was never affected, only its description.
  • Added: sdkVerifyWebbyxOneIdentity — exchanges a WebbyX One (IAM One) SSO ticket for the caller’s verified identity and stores it on the Session, so a workflow’s router/condition nodes can branch on {{session.webbyxOneIdentity.companyIds}} and similar fields. No <vai-avatar> embeddable SDK component ships a “Sign in with WebbyX One” affordance yet — Wetel’s own dashboard (Agent Testing screen) has a reference implementation of this exact flow if you want to see it working end to end before building your own.
  • Changed: Bedrock Mantle is now the platform default LLM provider (previously opt-in only via llmProviderOverride: BEDROCK_MANTLE, as noted below). If you never set llmProviderOverride on your agents or tenant, conversation turns are now served by Bedrock Mantle by default rather than the prior default provider — this can change latency, reply phrasing, and cost characteristics even without any change on your end. Every provider in the fallback chain (Bedrock Mantle → WebbyxAI/Claude Haiku → Gemini) still activates automatically on failure, so this is not a behavior change for reliability, only for which provider normally serves a healthy request. If you need the prior default back for your account, set llmProviderOverride explicitly — see Agents: LLM provider override.
  • Added: llm workflow nodes now support matchAnyOf — set it to the full list of valid labels on a classification node feeding a router/condition, and the executor will extract whichever candidate label appears earliest in the model’s raw response instead of trusting the model to emit only that word. Fixes a real reliability gap: some models (particularly smaller/faster ones) occasionally ignore a “respond with ONLY one word” instruction and answer the user’s actual question instead, which previously caused a silent misroute to the router’s defaultTarget with no error anywhere. See LLM Node: matchAnyOf.
  • Fixed: deactivateChannelConnector now releases the connector’s claim on its platform identifier (bot id, phone number, etc.) instead of holding it indefinitely. Previously, once any connector claimed an identifier, no connector — including a reactivated version of the same one — could ever claim it again without direct database intervention, since deactivating only paused the connector without freeing the identifier. If you deactivate a connector and later want to reactivate it, call updateChannelConnectorCredentials again first — a bare activateChannelConnector on a connector with no externalIdentifier now fails clearly instead of assuming the old registration still holds.
  • Added: sendMessage/sdkSendMessage now accept an optional clientTurnId string, echoed back verbatim on the AiResponseEvent produced for that turn. Useful for matching a specific reply to the message that produced it, especially if your integration sends more than one message before waiting for a response — previously there was no way to correlate a reply to a specific turn beyond assuming strict send/receive ordering. Fully optional and backward compatible: omit it and nothing changes. See Sessions: sdkSendMessage and Events & Subscriptions.
  • Added (pilot): Agents and tenants can now override which language model provider answers conversation turns, via llmProviderOverride (BEDROCK_MANTLE, WEBBYXAI_HAIKU, or GEMINI_DIRECT) and, for BEDROCK_MANTLE specifically, llmModelOverride to pick a specific model from AWS Bedrock’s open-weight catalog. Currently in limited pilot availability — contact your Wetel representative to enable this for your account. Every agent is unaffected unless both fields are explicitly set; any misconfigured or unavailable override falls back to the platform default automatically. See Agents: LLM provider override.
  • Added: Channel Connectors — a packaged, zero-code way to connect an Agent directly to Telegram. Five new operations: createChannelConnector, updateChannelConnectorCredentials, activateChannelConnector, deactivateChannelConnector, and listChannelConnectors. Activation runs a live health check against the platform and automatically registers the webhook for you — no manual setup step. WhatsApp, Slack, and Lark are valid ChannelType values but not yet implemented; every operation against one currently returns a clear error rather than a silent no-op. See Channels and Headless Agents: Telegram is now a packaged, zero-code connector.
  • Fixed (docs correction + real behavior): register now always assigns a new tenant to a freshly registered user automatically — this page previously (and correctly, at the time) documented that a fresh signup starts with no tenant (myTenant returning null). If your integration was creating or assigning a tenant itself immediately after calling register, check myTenant first — one already exists by the time register returns, and creating a second is unnecessary. login also self-heals this for any pre-existing account that was created before this change and never got a tenant assigned.
  • Added: ExportRecordDto.status now has a FALLBACK_READY value, alongside the existing PENDING/RENDERING/READY/FAILED. When a requestStudyKitExport render fails (including timing out) and a fallback document is configured server-side, the export settles into FALLBACK_READY instead of FAILEDdownloadUrl is populated with a fresh presigned URL to a fixed substitute document (7-day expiry, vs. the normal READY path’s 24h), and errorMessage is also populated so you can tell this happened. Treat FALLBACK_READY as a terminal, download-available state alongside READY when polling exportRecord. See Evaluation & Export: The ExportRecordDto type.
  • Added: llm workflow nodes now support excludeHistory — set true on a narrow “extract one value from the current message” node to exclude it from receiving the session’s conversationHistory in its underlying model call. Fixes a real reliability gap: once a session has a few tool-backed turns behind it, structured JSON response envelopes accumulate in that history, and a narrow extraction node with no legitimate use for that context can start echoing JSON-shaped output back instead of the bare value its own prompt asked for. See LLM Node: excludeHistory.
  • Added: llm workflow nodes with knowledgeBaseId set now support allowGeneralKnowledgeFallback — softens the default retrieval-augmentation directive (“answer using ONLY this reference material”) to “use it if relevant, otherwise answer from general knowledge and say so,” for nodes whose own prompt already has an explicit fallback instruction. Without this, a node like a study-kit generator would get overridden into refusing to answer whenever retrieval returned even a loosely-related chunk. See LLM Node: allowGeneralKnowledgeFallback.
  • Added: a new jsonString Handlebars helper, usable inside any text-field template (a response node’s messageTemplate, an llm node’s prompt, etc.) to safely embed a variable inside a hand-authored JSON string literal. Closes a gap in the existing text-field/JSON-field escaping split for workflows that render a JSON envelope as their outgoing message text. See Best Practices: building a hand-authored JSON envelope inside a text field.
  • Fixed: an agent with both openingGreeting set and a workflowId attached could produce a double greeting on session start — the static greeting racing against the workflow’s own first response. openingGreeting is now always skipped for any agent with a workflow attached (it was already effectively unreliable in this configuration; this makes the behavior deterministic instead of a race). If your agent needs a specific first-turn behavior and has a workflow attached, build it into the workflow graph itself. See Agents: openingGreeting.
  • Corrected (docs): Events & Subscriptions had the sessionEvents subscription’s $sessionId variable typed as Int! in its own example — wrong, it’s ID!. Every other session-related field in the API (SdkSendMessageInput.sessionId, etc.) genuinely is Int!, which makes Int! the natural but incorrect guess here; a variable declared with the wrong named type fails GraphQL schema validation and the subscription never opens, with no reply ever arriving. Two real integrations hit this before it was caught. No API change — the schema was always ID!; only the docs’ own example was wrong. Added an explicit callout so this doesn’t recur.
  • Added (docs): Avatar rendering options now explains that HOSTED_API is a rendering tier with more than one possible underlying provider (a higher-fidelity photorealistic option and a lower-latency/lower-cost lightweight option), selected server-side per agent — your integration code doesn’t need to know or branch on which one is active.
  • Added (docs): Headless Agents now includes a full worked example of bridging a messaging channel (Telegram, as the concrete case) to a headless Wetel session — long-polling instead of a webhook (no public URL needed), session-per-conversation mapping, and the subscribe-before-send pattern required since sdkSendMessage only returns an ack.
  • Clarified (docs): Backend-Proxied Relay Architecture now makes explicit that adopting this pattern does not, by itself, persist any messages to your database — it only gives you the place to add that write. A real integration built this pattern’s transport (subscription + SSE relay) correctly but never added the actual persistence, then assumed it was “basically already happening” since every message visibly passed through their backend. No API changes — this is a docs clarification to prevent the same assumption elsewhere. See the new callout under “What this pattern does not give you for free.”
  • Added (docs): Workflows Overview now includes a comparison to general-purpose state-machine libraries (XState and statecharts in general) — what Wetel’s workflow graph gives you for free (built-in LLM/tool/webhook node types, hosted cross-turn persistence) versus what it intentionally doesn’t attempt (hierarchical/parallel states, offline simulation tooling, portability outside the platform). No API changes — this is purely a docs addition for anyone evaluating the platform against familiar state-machine tooling.
  • Added (docs): Backend-Proxied Relay Architecture — a documented alternative to the default browser-direct integration. Your backend holds the sessionEvents subscription itself and relays replies to your frontend over a channel you control, so the browser never talks to Wetel directly. No API changes — this documents an architecture choice available today using existing mutations, for integrations that want backend-native message persistence, moderation before delivery, or centralized reconnection handling.
  • Added: Agents can now optionally use a hosted photorealistic avatar backend via the avatarBackend configuration field, as an alternative to the default client-side 3D avatar. Set avatarBackend: HOSTED_API when creating or updating an agent to enable this option. Currently in limited pilot availability — contact your Wetel representative to enable for your account. See Agents: avatarBackend and Avatar Rendering Options for details.
  • Added: Barge-in/interrupt now works on the pilot HOSTED_API avatar backend, not just the default 3D avatar — previously only <vai-avatar>’s TalkingHead-based (CLIENT_3D) path supported stopping the avatar mid-reply. Also added two new <vai-avatar> methods: handleBargeIn() for triggering an interrupt directly (e.g. an explicit “stop” button, independent of voice detection) and sendText(text) for sending a typed message through the same pipeline as a spoken one. See Avatar: handleBargeIn() and Avatar: sendText().
  • Fixed: Interrupting an agent mid-reply could permanently break turn-based routing for the rest of that session — every subsequent message received the same generic fallback reply regardless of content, with no error surfaced anywhere. This affected any agent whose workflow uses a ROUTER node keyed on turn number. No API surface changed; if you build workflows with turn-count-based routing and use interruptSession, this is now safe.
  • Added: AiResponseEvent now has a turnComplete field, alongside the existing isFinal. If your agent is workflow-driven and can speak multiple lines in one turn (e.g. a workflow with two response nodes in sequence), you previously had no way to distinguish “this line is done” from “the whole turn is done” — every line arrived with isFinal: true on its own. Watch turnComplete instead of isFinal if you need to know when the agent has genuinely finished replying. See Events & Subscriptions for the full explanation. isFinal is unchanged — this is additive, not a breaking change.
  • Added: sessionsByExternalCustomerId(externalCustomerId, first, after) — a new API-key-tier query for re-pulling all Sessions matching your own clientExternalId/clientId, Relay-paginated. Useful as a backup/re-sync path if you lose local state and never persisted session ids yourself. See Sessions: API-key-tier customer lookup queries.
  • Added: customerUsageSummary(externalCustomerId) — a new API-key-tier query returning rolled-up tokensIn/tokensOut/costUsd across every Session for one customer. Same page linked above.
  • Added: MessageDto now has tokensIn, tokensOut, and costUsd fields, populated per message (0 for USER messages and for workflow response nodes with no LLM call behind them). These are character-based estimates, not exact billing figures.
  • Added: Named API keys (createNamedApiKey(name) / revokeNamedApiKey(id)) — create multiple independently-revocable keys per tenant instead of sharing one generateApiKey key across every integration. Revoking one no longer affects any other. Fully additive — your existing generateApiKey key keeps working unchanged if you don’t use this. See Authentication: Named API keys.
  • Fixed: interruptSession now accepts either a dashboard JWT or the SDK avatarToken from sdkStart — previously it only accepted a JWT, which meant the <vai-avatar> embed SDK’s own barge-in call could never authenticate. If you’ve built directly against interruptSession using the avatarToken and seen an Unauthorized error, this is now fixed.
  • Fixed: the sessionEvents subscription’s $sessionId variable type is ID!, not Int! — if you hand-wrote this subscription against an older code sample, update the variable type or the subscription will fail to establish with no visible error (the triggering mutation still returns success, so this looked exactly like “no reply ever arrives”).
  • Fixed: publishing a workflow with two nodes sharing the same node ID, or with a node that has no path from START, is now rejected with a clear error at publish time — previously the duplicate silently became dead, unexecuted data, and the orphan just sat in the graph doing nothing. A workflow with an intentional loop-back (e.g. a router retrying a step) is unaffected — cycles are a supported pattern, not something this check rejects.
  • Fixed: a workflow condition node with no matching true/false outgoing edge now fails the run with a clear error, instead of silently stopping with no signal at all.
  • Fixed: publishWorkflow now validates every node’s configuration before publishing (previously only saving a draft did this) — a workflow can no longer go live with invalid node config.
  • Added: webhook nodes now support a configurable timeoutMs, matching tool/action nodes.
  • Improved: MCP connector failures are now classified (DNS resolution failed, connection refused, connection timed out, TLS handshake failed, authentication failed) instead of a single generic, undifferentiated error.
  • Fixed: sending a message to an ended session now returns a clear error instead of a generic server error.
  • Fixed: an internal inconsistency in how session-related mutations typed their ID argument has been standardized.
  • Fixed: a workflow node’s outputVar can no longer silently collide with a reserved internal name — this is now rejected at save time with a clear error, instead of corrupting the running session silently.

Embeddings API, external conversation sync, and AI-generated workflow drafts all shipped in the weeks prior to this changelog’s start — see the relevant guide pages for current behavior. A full historical changelog before this date isn’t available yet.