Skip to content

Changelog

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.

  • Added, live in production: an administrator can now restrict a tenant to a list of LLM models (for example Claude Haiku only). The llmModelCatalog query returns only the allowed models, myTenant { allowedLlmModels } shows the list (null means no restriction, the default), and a blocked model is refused with a clear error on generateText, the playground and llm nodes, and when saving an agent or publishing a workflow. Nothing changes for any tenant until a list is set. See Restricting which models a tenant can use.
  • Fixed, live in production: requests with a body over about 1 MB (for example an LLM request carrying a large base64 image) were rejected with HTTP 413 by the gateway, which browsers showed as a CORS error. The limit is now 10 MB. See the LLM API request size note.
  • Docs (new page): Choosing Languages and Voices explains primary vs alternative speech-to-text languages, the three-alternative limit, the speech tiers, and how a voice name relates to language and tier. It links to Google’s own language and voice lists as the source of truth. Also corrected the /stt page, which said Mandarin and Cantonese “will be removed” from the default fallback set; they have not been in it since 2026-10-01.
  • Changed, live in production: POST /stt now transcribes with Gemini by default and falls back to Cloud Speech-to-Text on any error. Expect confidence: 0 (“not reported”) and your languageCode echoed back when Gemini answers. POST /stt/google still gives the previous behaviour exactly.
  • Added, live in production: the WEBBYXAI_HAIKU provider can serve Claude Sonnet 4.6 and Claude Opus 4.6 (with -low, -medium, -high and -max effort variants) through modelOverride, llmModelOverride and generateText. Same per-token prices as Anthropic’s list (Sonnet 3/3 / 15, Opus 5/5 / 25 per million tokens in / out). They share the same model quota as Haiku, so use them sparingly.
  • Added, live in production since 2026-10-01: the default model provider (Claude Haiku) and the other Claude models it serves now read images. An llm node’s imageVars works on a Haiku agent with no provider change, up to 100 images per call (each under 5 MB as base64). The per-node limit stays 4 by default and the platform operator can raise it to 20, with 18 MB combined. Gemini is still available per node through modelOverride and answers automatically if an image is refused. On a quick digit-reading check Haiku scored 9 of 10 and Gemini 10 of 10.
  • Added, live in production since 2026-10-01: POST /tts/gemini, an optional Gemini text-to-speech route for voice notes. WAV output, no word timings (not for lip-sync), about 3 to 4 seconds per reply. /tts and /tts/google are unchanged.
  • Behaviour change, live in production since 2026-10-01: end_session’s runEvaluation is now honoured, and leaving it out means no evaluation. It was always documented as an opt-in but the platform ignored it, so every end_session evaluated and a node that said runEvaluation: false still did. Both false and an omitted field are live. If your workflow omits the field and you read its evaluations, add "runEvaluation": true. Sessions ended through sdkEndSession still return their evaluation. Chat flows that end a session after each exchange save up to two model calls per session.
  • Added, live in production since 2026-10-01: POST /stt/gemini and POST /stt/google let you pick the speech-to-text engine per call. /stt/gemini uses a Gemini speech model: faster in our checks, capitalised and punctuated text, no language hint needed. confidence is 0 (“not reported”) and the response languageCode echoes your request. Plain POST /stt is unchanged and keeps using Cloud Speech-to-Text unless the operator changes the default.
  • Added, live in production since 2026-10-01: an llm node’s modelOverride can now name a model another provider serves, and that one node runs on that provider. Use it to give a single image node a Gemini model inside an agent that otherwise runs on Haiku. Before, naming another provider’s model failed the node. A model no provider serves still fails it.
  • Added, live in production since 2026-10-01: runWorkflowTask and scheduled workflow fires now leave an invocation record (agent, source, your externalConversationId, the names of the variables sent, accepted or rejected with a fixed reason code, and a link to the resulting run) so support can trace what an integration fired. Variable values, attachment details and error text are never stored. No change to the mutation’s arguments or result, and nothing for you to do. If you put something sensitive in externalConversationId, stop: it is now stored as sent.
  • Added, live in production since 2026-10-01: myAuditLogs, the signed-in user’s own recorded actions in their workspace, newest first, with cursor paging and optional auditableType and action filters. Only field names are recorded, never values, and you only see your own actions. Recording now covers agents, API keys, workflows (including publish), custom actions, MCP connectors, channel connectors, scheduled workflows, knowledge bases and workspace settings; before, only agent updates were recorded. Additive: existing queries are unaffected.
  • Behaviour change, live in production since 2026-10-01: agentAuditLogs no longer returns values for credential-like agent fields. externalSyncUrl, externalSyncSecretRef, externalSyncOwnerId, externalSyncSchemaConfig and avatarBackendConfig come back as { from: null, to: null }, including in older entries, and are no longer stored. Prompts and other ordinary settings still show their old and new values. The history also now includes the agent’s creation and deletion, which carry no field values.
  • Added, live in production since 2026-10-01: apiUsageSummary breakdown rows gain creditsUsed: Int!, the credits each (model, operation) item consumed in the selected month. It uses the same rule as the item’s bucket and the rows in a bucket sum exactly to that bucket’s creditsUsed (LLM credits are approximate; tool calls, speech and avatar are real charges). costUsd is unchanged and still returned. Additive, so existing queries keep working; select creditsUsed only once your target environment has this release, since an unknown field fails the whole query.
  • Added, live in production since 2026-10-01: sessionActivitySummaries can now be sorted on the server. New optional arguments sortBy (CREATED_AT, STARTED_AT, ENDED_AT, STATUS, CLIENT_EXTERNAL_ID, AGENT_NAME, EVALUATION_SCORE) and sortDirection (ASC, DESC). Rows with no value for the sort field come last in both directions, and pagination cursors are tied to the sort they were issued under. Without the new arguments nothing changes: newest first, and existing cursors keep working. Last-message time and latest-run status can’t be sorted yet.
  • Behaviour change + added, live in production since 2026-10-01: apiUsageSummary breakdown is now scoped to one calendar month. It used to count every call since the tenant was created, even though the dashboard labelled it “this month”. It also no longer truncates model names that contain a colon. New optional argument yearMonth: String (YYYY-MM, past or current month) scopes every figure in the response to that month, so you can look at previous months. New fields: yearMonth on the summary and costUsd on each breakdown row. Existing field names are unchanged. If you were reading breakdown as lifetime totals, sum it across months yourself.
  • Live in production since 2026-10-01: Per-agent speech-to-text languages. Agent, createAgent and updateAgent gain sttLanguageCode (the STT primary; null = use ttsLang) and sttAlternativeLanguageCodes (null = platform default English + Malay, [] = primary only, otherwise at most 3 entries, no duplicates, not containing the primary, BCP-47 shaped). Sending null on updateAgent resets a field to its default. Telegram voice notes now honour both fields. To bring Mandarin detection back for an agent, set sttAlternativeLanguageCodes: ["cmn-Hans-CN"].
  • Live in production since 2026-10-01: sdkStart and startAvatarDemoSession return two new fields, sttLanguageCode: String! (already resolved: agent setting, else ttsLang, else en-US) and sttAlternativeLanguageCodes: [String!]. Both are additive. <vai-avatar> gains matching stt-lang and stt-alternative-langs attributes.
  • Behaviour change, live in production since 2026-10-01: <vai-avatar> used to always send en-US as the STT primary. When stt-lang is not set it now follows tts-lang. Pages that set neither attribute are unaffected. Set stt-lang explicitly if your agent speaks one language but callers speak another.
  • Live in production since 2026-10-01: POST /stt now carries a fallback re-check guard: when a fallback language wins the recognition but produces an empty transcript or mixed-script garbage (a small Han fragment plus English text), short clips (under ~50 seconds) are automatically re-checked once in the primary language only. If the primary-only result is at least as good, it replaces the fallback’s result. The response now includes attempts: 1 or attempts: 2 so you can meter this — each recognition call is billed per 15-second audio block. Language reporting is now standardised: unrequested Chinese winners are reported in BCP-47 casing (cmn-Hans-CN, yue-Hant-HK) instead of all-lowercase; and requested Chinese languages are reported the way you spelled them in the request (so zh-CN comes back as zh-CN, not normalized to cmn-Hans-CN or anything else). Known limits: (1) Cantonese on a Mandarin primary counts as the primary and is never retried. (2) Japanese fallbacks are re-checked only for empty transcripts, not mixed-script. (3) Genuine code-switching (Mandarin with English phrases woven in) can trigger a re-check but the original is kept unless the primary-only result is at least as confident. alternativeLanguageCodes is now protected: any Chinese variant (zh, zh-CN, zh-TW, zh-HK, cmn-*, yue-*) in the list is guarded by the re-check, even if your primary is not Chinese.
  • Already live: The default fallback language set for POST /stt changed from [en-US, cmn-Hans-CN, ms-MY, yue-Hant-HK] to [en-US, ms-MY] only (shipping 2026-09-29, now live in production and staging). Mandarin and Cantonese were removed because they trigger false-positive language misclassifications on accented English clips — even fluent English like "Hi testing one two three" the provider sometimes tags as cmn-Hans-CN or yue-Hant-HK. The language-tag correction (fall back to non-Han when the winning language’s script doesn’t match its transcript) still exists, but removing them from the defaults prevents the mislabel entirely. If your integration needs Chinese detection, pass it explicitly: "alternativeLanguageCodes": ["cmn-Hans-CN"] or ["yue-Hant-HK"] (or both, using up to 3 slots). The same changes apply to the LLM Playground’s transcribeSpeech query.
  • Already live, undocumented: POST /stt’s OGG_OPUS encoding now auto-detects sample rate from the file header itself — the sampleRateHertz field is now genuinely optional for OGG/Opus files. Different source apps (Telegram, WhatsApp, etc.) record at different real rates, and guessing one value could never be right for every source. If you were passing sampleRateHertz explicitly as a workaround for empty-transcript results on some sources, you can now leave it unset and let the provider read the real rate from the file.
  • Added: Lark joins OmniChat/Nortia/aisCRM as a certified-operation product — certifiedOperations(product: "LARK"). Unlike the other three (Wetel’s own shared house accounts), Lark is a per-tenant app integration — bring your own Lark app (larkAppId/larkAppSecret, already self-serve via createCustomAction, see Using Lark) and certified Lark operations work against it directly, no WebbyX One sign-in step needed. Shipped same-day as 14 operations, not just one: Messaging (Send Message — execution-verified live against Lark Suite’s international API; Reply to Message, Get Message), Chats (Create Group Chat, Get Chat Info, Add Chat Member), Contacts (Get User Info, Batch Get User by Email/Mobile), Calendar (List/Create Events), Approval (Create/Get Instance), and Bitable (List/Create Records — Bitable’s own request/response envelope is fixed and certifiable even though a tenant’s table columns aren’t). Everything past Send Message is schema-verified against Lark’s official public API reference rather than execution-verified, since a live write there (a real group chat, calendar event, approval instance, or Bitable row) has no clean rollback — check an operation’s own verificationNote.
  • Changed (every session, read this if you send messages quickly): A new message now supersedes a turn that is still in progress on the same session. Before, the two ran at the same time, and the earlier one’s reply could arrive last, answering a question the user had moved past. Now the earlier turn stops at its next node boundary (or next streamed sentence, for a direct non-workflow agent). Its reply is neither published nor saved to the transcript, and only the newer message is answered. No InterruptedEvent is sent for a supersede, and the superseded workflow run is recorded as INTERRUPTED. Three things to check on your side. (1) If you correlate replies by clientTurnId, a superseded message never gets one, so stop waiting for it once a later reply arrives. (2) A turn that has already sent its first reply (an early acknowledgement from a response node) is never superseded and runs to completion alongside the newer message. A node already running, such as an action call, always finishes too. (3) The residual to design around: a workflow that performs a side effect before saying anything can have that step skipped if the user double-texts first. Put a response acknowledgement before any action that must not be skipped. Explicit interruptSession barge-ins now also drop a reply still being generated. Resumed await_reply turns, runWorkflowTask and scheduled runs are unaffected. See When a user sends a second message before the first reply.
  • Added: Two new voiceTier values — CHIRP3_HD and STUDIO. CHIRP3_HD is Google’s current-generation voice, positioned for conversational use, at a modest cost increase over NEURAL2. STUDIO is available but not recommended for a live voice session — Google tunes it for long-form narration, not dialogue, and it costs substantially more. Set either directly on AgentDto/UpsertAvatarConfigInput, same as any other tier.
  • Fixed: POST /stt could return the wrong languageCode on an otherwise-correct transcript — a fully English transcript (e.g. "Hi testing one two three") sometimes came back tagged cmn-Hans-CN or yue-Hant-HK. The tag is now corrected when the winning language’s script doesn’t match its own transcript (a Han-script language with zero Han characters in the text it produced). This does not change the transcript itself, and does not affect a transcript that’s genuinely mixed-script. Separately, if you’re seeing garbled (not just mistagged) transcripts: send your best actual guess as the primary languageCode, not a placeholder — real testing found the primary carries much more weight in recognition than the fallback list does, and narrowing the fallback list does not help.
  • Added: POST /stt now falls back across languages instead of recognising in exactly one. Every call is sent with English (en-US), Mandarin (cmn-Hans-CN), Malay (ms-MY) and Cantonese (yue-Hant-HK) as fallbacks, minus your primary languageCode and capped at 3. The response’s languageCode is now the language actually detected, where it used to echo the request. Before, a Mandarin or Malay voice note sent with en-US didn’t error; it returned a short garbage transcript. On our synthetic test clips, Mandarin and Malay went from a garbage transcript to correct on every run, and Cantonese went from a garbage transcript to correct on about half of runs, with the rest coming back empty. A new optional alternativeLanguageCodes field overrides the list (max 3, since the speech provider silently ignores the whole list past that, so a 4th is rejected with a 400). Send [] to turn fallback off. Two things to handle: a transcript recognised through a fallback language can come back with no confidence, reported as confidence: 0, so apply any low-confidence cutoff only when confidence > 0. And a clip that switches language mid-sentence is still transcribed in one dominant language. Hokkien and Hakka are not supported. Real accented voice notes haven’t been measured yet, only synthetic voices, so check against your own traffic. The LLM Playground’s transcribeSpeech also returns the detected language now.
  • Added / Deprecated: sessionActivitySummaries — a Relay-paginated, newest-first session list with agentId/status/search filters and totalCount, built for the new Supervision console. sessions is now @deprecated in favor of it — it still works, but returns every session the tenant has ever had with no pagination, which is a real problem for a tenant with a long history. Migrate any dashboard-style session list to sessionActivitySummaries.
  • Fixed: An llm node’s modelOverride now takes effect. Until now the field was saved but never read, so every llm node silently ran on the agent’s or tenant’s model regardless of what it was set to. The model a node runs on now resolves node modelOverride → agent llmModelOverride → tenant llmModelOverride → the provider’s default, most specific first. Two things to know before relying on it. (1) It picks a model, never a provider: the provider still comes from the agent/tenant llmProviderOverride, so a node can’t switch a non-vision provider into a vision one by itself. This corrects the 2026-09-17 imageVars entry below, which said setting modelOverride was enough. (2) The override is checked against the resolved provider’s model catalog (the same list the dashboard’s model picker shows). A model id that provider doesn’t serve now fails the run with an error naming the problem, where it would previously have been answered quietly by a different model. If you set modelOverride on nodes earlier, check those ids are valid for your provider before your next run.
  • Added: Scheduled Workflows: run an agent’s published workflow on a cron schedule, with no inbound message. Seven new dashboard-JWT operations: createScheduledWorkflow, updateScheduledWorkflow, pauseScheduledWorkflow, resumeScheduledWorkflow, deleteScheduledWorkflow, scheduledWorkflow, scheduledWorkflows. Each fire is a headless run with the same starting context as runWorkflowTask. Cron is strict 5-field only, so seconds-level and @daily-style aliases are rejected at save time, and the cron is evaluated in an IANA timezone you choose (default UTC). Three cost controls apply to every schedule: a per-plan limit on the number of schedules (paused and auto-disabled ones still count), the monthly LLM spend cap checked before every fire, and an automatic stop after N consecutive failed fires (maxConsecutiveFailures, default 3). Known limitation: an auto-disabled schedule sends no notification yet, no email and no webhook. Poll scheduledWorkflows for status: AUTO_DISABLED if a schedule matters to you.
  • Fixed: POST /stt no longer drops the rest of a voice message after a pause. For audio under ~50 seconds, which covers most real voice notes, the endpoint kept only the first transcript segment the speech provider returned. The provider typically starts a new segment at a pause, so everything after the first pause was silently lost, with a normal 200 and a normal-looking confidence. All segments are now joined into one transcript, and confidence is averaged across them, matching how longer audio was already handled. This affected every /stt caller, including the Telegram channel connector and the <vai-avatar> voice widget. If you were working around “the agent only heard the first half of my message,” you can drop the workaround. See Sending Voice Messages to an Agent.
  • Added: Six vision-capable models now available via BEDROCK_MANTLE (in addition to the existing GEMINI_DIRECT option). Set llmProviderOverride: BEDROCK_MANTLE and llmModelOverride to one of: mistral.mistral-large-3-675b-instruct, nvidia.nemotron-nano-12b-v2, mistral.magistral-small-2509, mistral.ministral-3-14b-instruct, google.gemma-3-27b-it, or moonshotai.kimi-k2.5. See LLM API: Vision-capable models for accuracy, latency, and implementation notes. Caveat: all testing to date has been on clean, synthetic images (high-contrast printed text); real-world accuracy on photographed documents may vary. Validate against your own image sources before committing to a model.
  • Docs (capability was already live, just undocumented): updateChannelConnectorSettings — toggles voice replies for a Telegram channel connector, either mirroring the input modality (voice in → voice out, text in → text out) or forcing every reply to be voice. This mutation and the ChannelConnectorDto.voiceReplyEnabled/alwaysVoiceReply fields shipped without a docs page; added now.
  • Fixed (same day, second pass): POST /stt’s OGG_OPUS handling now auto-detects the real sample rate from the audio file itself, instead of assuming one fixed value. Voice messages from different source apps can be recorded at genuinely different real sample rates (confirmed: Telegram’s own voice notes are 48kHz) — assuming a single value, even a corrected one, could never be right for every source. If you were previously working around empty-transcript results for some voice-note sources by explicitly passing sampleRateHertz, you can now leave it unset — see Sending Voice Messages to an Agent.
  • Docs (clarification, same day follow-up): the /stt fix below is confirmed working up to ~5 minutes of audio. A much longer clip (10+ minutes) could still hit an upstream limit and fail with an explicit 500 — not silently. If your integration handles very long recordings (e.g. no hard cap on user-recorded voice messages), see the updated constraints on Sending Voice Messages to an Agent and reach out if you need support for longer audio.
  • Fixed: POST /stt no longer fails on voice messages longer than ~60 seconds. Previously this endpoint used synchronous transcription exclusively, which silently rejected anything over a hard ~60-second ceiling with no clean way to work around it (the underlying speech provider’s own limit for that call shape). It now automatically routes longer audio through an asynchronous long-running path instead — same request/response shape, no chunking, just proportionally longer latency for longer audio. See Sending Voice Messages to an Agent for the updated constraints.
  • Docs fix: generateText’s images field was documented as “only consumed by BEDROCK_MANTLE” — this was stale. GEMINI_DIRECT also has native inline-image support and always has. No API behavior changed; only the docs were wrong.
  • Breaking: generateText is now a Mutation, not a Query. Migration: change query { generateText(...) } to mutation { generateText(...) } in your client — the field name, arguments, and response shape are all unchanged, only the operation keyword. This is architecturally correct for a billable action (it now matches startTextGeneration’s existing shape), but it is a real breaking change for any client still selecting it as a query. No known caller has integrated against generateText yet (it shipped 2026-08-27, ~4 weeks before this change); if you have, this needs a one-line fix on your side.
  • Changed: generateText now accepts either a dashboard JWT or an X-Api-Key header (was X-Api-Key only) — a dashboard-authenticated caller can now use it directly, alongside the existing API-key partner path. startTextGeneration and textGenerationEvents are unaffected — still X-Api-Key only.
  • Added: GenerateTextResultDto gained a new field, creditsUsedApprox (Int!) — an approximate credit-equivalent of costUsd, for previewing what a call cost under the credits pricing model. A blended estimate, not an exact per-model breakdown.
  • Added: A second, independent rate-limit ceiling now applies to generateText — 60 requests/minute per tenant, on top of its existing per-client-IP limit (raised from 10/min to 30/min in the same change). Both must pass. This is the one operation on the whole Rate Limiting page keyed partly by tenant rather than client IP alone, so a partner backend proxying many end users through one server IP isn’t bottlenecked by the IP tier. See Rate Limiting: LLM API.
  • 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 scenarios — captureRegressionScenario 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 FAILED — downloadUrl 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.