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.
2026-09-23
Section titled “2026-09-23”- Docs (clarification):
updateWorkflow— spelled out explicitly thatnodes/edgesare 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 touchedconfigon some nodes silently wipedpositionfrom all of them.
2026-09-22
Section titled “2026-09-22”- Added: The
webhooknode can now embed a fetched file as base64 into its ownbodyTemplate, via a newfetchAsBase64field — up to 4 entries, each{ urlTemplate, bodyField }, fetching either anhttp(s)://URL or ans3://bucket/keyURI and injecting the base64 string at a dot path in the rendered JSON body. Until now this capability only existed on theactionnode (fetchAsBase64Var) — awebhooknode 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 touchesctx(it’s injected straight into the request body), whileaction’s writes the base64 string to a context variable. Also new:actionnode’s existingfetchAsBase64Varnow accepts ans3://bucket/keyURI too, not just HTTP(S) — useful when the file already lives in your own S3 bucket rather than being reachable over the public internet. Ans3://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
maxVisitsfield on the node’s ownconfig— available on every node type, defaulting to1, capped at100, 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 reportedCOMPLETED. If you drew a retry loop and wondered why it never retried, that is why. Set"maxVisits": 3on 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 declaredmaxVisitsand has no escape edge, so the runFAILEDs with a named error rather than quietly stopping; (3) the node never declaredmaxVisits, in which case that branch stops and the rest of the graph carries on exactly as before. All three now write anodeTraceentry withstatus: "VISIT_LIMIT_REACHED",visitLimitandvisitCount, where previously there was no record of any kind. Nothing changes for a graph that doesn’t opt in — an unsetmaxVisitsreproduces 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:maxVisitshas 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;nodeTracemay now hold more than one entry for the samenodeId, 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 reportsBUDGET_EXCEEDED, notVISIT_LIMIT_REACHED. The dashboard’s workflow builder has no input formaxVisitsyet — set it in thenodesJSON you pass toupdateWorkflow(nodeConfigSchemaalready 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 andVISIT_LIMIT_REACHEDfor 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 atstart, 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 everyawait_replynode) andunanswered(required, and only reachable, whenmatchModeis'options'); both rules are checked at save/publish time, not mid-conversation.free_textmode takes any reply as the answer;optionsmode 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 takesunansweredinstead of guessing. New surface, all additive:pendingAwaitReplies(sessionId: Int)(reference) lists what is paused and until when;WorkflowRunStatus.AWAITING_INPUTis the paused run’s status (notCOMPLETED— exclude it from any “non-completed run = failure” alerting you have); andWorkflowRunDto.resumedFromRunIdlinks the continuation run back to the paused one. Three things to know before adopting it: (1) the question is published withturnComplete: true, because the turn genuinely is over until a human replies — a client using a quiet-period/silence timer instead ofturnCompleteto 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 downunanswered, 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 viaupdateWorkflow’snodesJSON (nodeConfigSchema(type: AWAIT_REPLY)already returns a real JSON Schema if you form-generate your own editor).
2026-09-21
Section titled “2026-09-21”- 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 extranodeTraceentry withstatus: "BUDGET_EXCEEDED"plusbudgetLimit("maxNodeExecutions"or"deadline"),budgetNodeExecutionsandbudgetElapsedMs, attributed to the node the engine refused to dispatch (that node never ran). Save time: 200 nodes / 400 edges max, rejected byupdateWorkflow(drafts included) andpublishWorkflowwithWorkflow 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 onentry.status === "BUDGET_EXCEEDED"rather than parsing the error text; the threebudget*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 owntimeoutMs, not by this. See Workflow Overview: Graph size and run limits and Workflows API: Run limits andBUDGET_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 isX-Api-Keyonly (no JWT), and the tenant is resolved from the key itself, so the input has no tenant field. Facts travel as a flatvariablesmap plus optionalattachmentUrl/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 aworkflowId: 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 fromworkflowRuns(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 terminalwebhooknode). Rate-limited at 60/min per client IP — see Rate Limiting: Workflow tasks. - Changed: A
conditionnode whose expression references a variable the current run’s context does not hold now evaluatesfalseand takes thefalseedge. 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 fromworkflowRuns(sessionId) { context }. Deliberately narrow: only an unresolved-variable reference maps tofalse— 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 > 0is now a complete guard covering absent,0,"0",""and prose in one expression — use it (neversomeId != 0, which does not coerce) in front of any node that writes outward.
2026-09-17
Section titled “2026-09-17”- 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-documentedPOST /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 validavatarTokenresolves a tenant. See Rate Limiting: Avatar service and the REST reference. - Docs (previously undocumented):
POST /avatar-session— negotiates aHOSTED_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/googlesaid its rate limit was “shared with/tts.” It isn’t —/ttsand/tts/googleeach have their own independent counter (@nestjs/throttlerkeys 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 asHeader: 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
actionnode config field,extractTextVar— an alternative output mode forfetchAsBase64Var’s fetch: instead of base64-encoding the fetched bytes intoctx[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 intoctx[extractTextVar]instead, withctx[outputVar]left unpopulated. Capped atextractTextMaxChars(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. RequiresfetchAsBase64Varto also be set. For mid-conversation document reading (a partner API hands back a file URL you want anllmnode to read), as distinct from Knowledge Base ingestion, which is for content you want retrievable across future sessions. - Added: New
llmnode 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, alongsidepromptTemplate’s text. Read this before using it: the platform’s default LLM provider does not support image input, and animageVars-bearing turn on an incapable provider fails the entire workflow run, not just the node —llmnodes have nosuccess/failureedge routing to fall back on the waytool/actionnodes do. SetmodelOverride, 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-injectingvai-avatar.jsin the same page session throwsSyntaxError: Identifier '...' has already been declared, because its top-levelconstdeclarations 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/imageVarspieces 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).
2026-09-15
Section titled “2026-09-15”- Added:
POST /sttnow acceptsOGG_OPUSas anaudioEncodingvalue, alongside the existingMP3/LINEAR16/WEBM_OPUS. This matters becauseOGG_OPUSis 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 whensampleRateHertzis omitted (matching WhatsApp’s native rate), distinct fromWEBM_OPUS’s 48kHz default. New walkthrough: Sending Voice Messages to an Agent — a copy-paste guide for transcribing a voice note with/sttand feeding it into an agent viasdkSendMessage, using theavatarTokenfromsdkStart(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 /sttoverstated 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.
2026-09-14
Section titled “2026-09-14”- Added:
refreshAvatarToken— renew a session’savatarTokenwithout starting a new session. AnavatarTokenlives about an hour, and until now there was no way to renew one: a conversation that outlived its token simply started failing withInvalid or expired avatar token, with no recovery path short of a freshsdkStart(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 asinput.avatarToken(not as anAuthorization: Bearerheader — this is the oneavatarTokenoperation that has to accept an already-expired token, which the normal header guard rejects outright); you get back a fresh full-TTL token, the samesessionId, andexpiresInMsso 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 beACTIVE, 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 livesessionEventsWebSocket —graphql-wsreadsconnectionParamsonly 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/sdkEndSessionneed 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
avatarTokenwas “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 gaprefreshAvatarTokenabove now closes.
2026-09-11
Section titled “2026-09-11”- Added:
credentialSecretHeaderNameoncreateCustomAction/updateCustomAction. A Custom Action’s storedbearerTokenused to only ever render asAuthorization: Bearer <token>— this lets you send it under any static header name instead (e.g.X-Api-Key,X-Client-Secret), with noBearerprefix and noAuthorizationheader sent, for third-party APIs that authenticate that way. Only applies to the plain self-servicebearerTokencredential; rejected on create unlessbearerTokenis 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 withextraHeaders. - Changed:
extractFirstIntegernow stores a real number inoutputVar, not a digit string — it used to be a string. Motivation: aconditionnode’s expression is evaluated with no type coercion, so a"0"string sentinel compared withmyVar != 0evaluatedtrue(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 of0before the partner API rejected them. If your workflow gates on this sentinel withmyVar != "0"or{{#if myVar}}, update it to a plainmyVar != 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
findIdhelper’sidFieldargument 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 same0“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 than21) is now coerced to a real number too —findIdalways returns a number or0. Motivation: a nestedcurrent_application.idlookup that a flat-field-onlyfindIdcouldn’t reach at all.
2026-09-10
Section titled “2026-09-10”- Changed: the
findIdHandlebars helper now matches in three tiers — exact, then normalised (case, whitespace, punctuation, diacritics, trailing plurals), then unique whole-token containment — and returns0whenever a tier is ambiguous instead of picking the first hit. Motivation: a live run where anllmnode wroteSolutions Architectfor a posting titledSolution 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 return0at every tier (previously first-wins). Details and the “route the0case 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 }ornull),channel({ trigger, nortiaEnabled, nortiaCompanyId, raw }), and flatattachmentUrl/attachmentMime/attachmentFilenamemirrors (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 ownllmnode to pullurl/mimeback out of the marker text, you can delete it — use the flat mirrors directly in aconditionexpression (they’re safe to compare even with no attachment present) and asfetchAsBase64Var’s source (it takes a bare variable name, not a dot path). Markers are not stripped fromuserMessage, 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) intosdkSendMessage, 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 withcredentialSource: "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. UsecredentialSource: "NORTIA"(Wetel-provisioned shared credential) instead;SESSION_NORTIAis 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
sdkStartas unthrottled (it has been 30/min since 2026-09-08) and omittedgenerateWorkflowDiff(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 (llmshould exposepromptTemplate, notsystemPrompt, as the main field;actionis a Custom Action + certified-operation picker, not a raw URL form;end_sessionhas no final-message field). - Docs:
sub_agentnow documents two requirements previously only discoverable at runtime — every target agent must have a published workflow (otherwise that target returnsstatus: '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 noeq/comparison helper — equality branching belongs in arouter/conditionnode.
2026-09-09
Section titled “2026-09-09”- Fixed (read this if a workflow using a Nortia certified operation is reading
current_status,reply, ordept_head_namefrom 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_statusis nowstatus(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 nowanswer;Department.dept_head_name(Update Department) is now a relation,dept_head { id name }, rather than a plain string field; andJobPosting.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’scertifiedOperationIdis 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.
2026-09-08
Section titled “2026-09-08”- Fixed (security — read this if any ACTION/WEBHOOK/TOOL/MCP target or
externalSyncUrlever redirects, or its hostname doesn’t resolve): Outbound calls made on your behalf —actionnodes,webhooknodes,tool/MCP connector calls,ingestDocumentFromUrl, andexternalSyncUrl— no longer follow HTTP redirects blindly. A redirect target is now either rejected outright (action/webhook, which setmaxRedirects: 0, since no legitimate target needs to redirect) or re-validated against the same private-address check before being followed (thefetch-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 youraction/webhookURL legitimately 301/302/307/308s (e.g. anhttp://→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
credentialSourceis a Wetel-provisioned, tenant-wide shared credential ("OMNICHAT"/"NORTIA"/"AISCRM"/"LARK_TENANT"),urlandmethodare now immutable viaupdateCustomAction— 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
avatarTokenreturned bysdkStartis now bound to the specific session it was issued for.sdkSendMessage,sdkEndSession,sdkVerifyWebbyxOneIdentity,requestStudyKitExport,interruptSession, and thesessionEventssubscription now reject any call whosesessionIddoesn’t match thesessionIdthe presentedavatarTokenwas actually minted for, withForbidden. Every integration that already uses the{sessionId, avatarToken}pair together exactly as returned from a singlesdkStartcall — 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, andsdkStart. 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:
generateOTPno longer accepts the optional top-levelaudienceargument — it has been removed from the schema entirely. If your integration ever passedaudiencealongsideinput(even the default"customer"value), drop it; the call now only takesinput. 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
generateOTPare now purpose-bound — a code minted for oneTokenPurpose(e.g.SIGN_UP) can no longer be spent against a different flow expecting a different purpose (e.g.forgotPasswordWithOTP’sRESET_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:
responsenodestructuredContentTemplateand the correspondingMessageDto.structuredContentfield. Aresponsenode 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,structuredContentis justnulland the text reply still sends. Dashboard-only consumer today; no interactive/button variant yet.
2026-09-07
Section titled “2026-09-07”- 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/startsession reset — ending the mapped session and replying with the agent’s ownopeningGreetingon/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
findIdHandlebars 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.
2026-09-06
Section titled “2026-09-06”- Added:
runRegressionScenarioandworkflowRegressionResults— 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.runRegressionScenarioreturns immediately (before the replay finishes, since it drives a real LLM turn); pollworkflowRegressionScenarios/workflowRegressionResultsa few seconds later for the outcome.
2026-09-05
Section titled “2026-09-05”- Added:
agentAuditLogs— this agent’s change history (who changed what, when). v1 coversupdateAgentonly — 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) orWETEL(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. SelectingWEBBYX_ONErequires the account already have a linked WebbyX One identity vialinkIdentity— throws otherwise.myTenantnow exposes the current value asprimaryBillingAccount.
2026-09-04
Section titled “2026-09-04”- Added: the certified operation catalog is no longer exclusively GraphQL — 2 new Nortia entries,
List Job Postings (REST)andSubmit Resume (REST), run against a separate plain REST API (api-recruitment.webbyx.dev’s “Integration API,” distinct from Nortia’s GraphQL admin API). NewoperationTypevalues"rest_get"/"rest_post"alongside the existing"query"/"mutation". For a REST certified operation,argsTemplateis 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’ ownverificationNote. Catalog total is now 172 (Nortia 75, OmniChat 59, aisCRM 38). - Added: a new
actionnode config field,fetchAsBase64Var— an alternative to the normalcustomActionId-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 toctx[outputVar]. Built specifically to feed a certified operation’sfile_base64-shaped request field (e.g. the newSubmit Resume (REST)above) without needing a real GraphQL-multipart file upload, which no executor in this platform supports today.customActionIdbecomes optional onactionnodes whenfetchAsBase64Varis set — every other field on the node is ignored in this mode. Same SSRF protection as every otheractionnode call; sends noAuthorizationheader, so only point it at a URL meant to be fetched without auth.
2026-09-03
Section titled “2026-09-03”- 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/webhooknode’s HTTP failure,nodeTrace’serrorfield now includes the partner endpoint’s own response body — its status code and JSONmessage/error/errorsfield (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 asrenderedRequest. Not retroactive. - Changed:
sdkSendMessage’s andsendMessage’stextfield limit raised to 10,000 characters (previously 4,000 forsdkSendMessage, 5,000 forsendMessage— 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 intotext), consider whether Wetel’s own AgentpersonaPromptand 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’sverificationNotebefore relying on one for anything beyond a controlled test. Catalog total is now 170 (Nortia 73, OmniChat 59, aisCRM 38). - Fixed:
createCustomAction/updateCustomActionnow accept aninput.credentialSourcefield, restricted to"SESSION_OMNICHAT"/"SESSION_AISCRM". Previously these two values were never actually reachable through either mutation — despite Credential sources already describingSESSION_OMNICHAT/SESSION_AISCRMas “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 withbearerToken/larkAppId+larkAppSecret; switching an existing action onto aSESSION_*source clears any stale stored credential.OMNICHAT/NORTIA/AISCRM(tenant-wide shared account) andLARK_TENANTremain not directly settable here — see the credential sources table for why. - Added:
createCustomAction/updateCustomAction’sinput.credentialSourcenow 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’sproductTickets), then reference the action’sid. No Wetel-provisioned shared account needed. See Credential sources and the certified operation catalog for the full picture.
2026-09-02
Section titled “2026-09-02”- Added:
renderedRequest(see below) now runs through a best-effort secret-redaction pass — JWTs,Bearertokens, 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 intoargsTemplate/bodyTemplate— use a Custom Action’s credential-source mechanism instead. - Added:
workflowRuns/workflowRun’snodeTraceentries now includerenderedRequestfortool,action, andwebhooknodes — 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 malformedargsTemplate/bodyTemplateon 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), andlinkIdentity/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 fromsdkVerifyWebbyxOneIdentity(session-scoped end-user identity linking for calling certified operations) — see the note on both pages if you’re unsure which one you need.
2026-09-01
Section titled “2026-09-01”- Added: Opt-in success/failure edges for
toolandactionnodes — draw one outgoing edge labeledsuccessand one labeledfailure(same mechanism condition nodes already use fortrue/false) and the runner routes tofailureon any thrown error instead of failing the whole run, writing the error message to a_lastNodeErrorcontext 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). Afailureedge requires a matchingsuccessedge; 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 —
captureRegressionScenariofreezes an already-completedWorkflowRunas a named baseline (reconstructed input script,nodeTracesnapshot, and derived structural assertions), andworkflowRegressionScenarioslists them per workflow. This exists to catch a real, documented failure mode — anintent/classification LLM node silently misrouting after a model change, with no error anywhere.
2026-08-30
Section titled “2026-08-30”- 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/certifiedOperationare confirmed live and returning real data on bothaws-stagingandaws-productionnow. If you hit this earlier, it’s resolved — try again with the id your owncertifiedOperationsquery returns (ids are per-environment; don’t hardcode one from a doc example).
2026-08-29
Section titled “2026-08-29”-
Added: the certified operation catalog — for OmniChat, Nortia, and aisCRM
actionnodes, pick a Wetel-verified GraphQL query or mutation fromcertifiedOperations/certifiedOperationinstead of hand-authoring the request body yourself. SetActionNodeConfig.certifiedOperationIdandargsTemplatebecomes that operation’s GraphQL variables. Also available from the Workflow Editor UI as a second dropdown on theactionnode’s config panel, once the chosen Custom Action’s credential source has a matching certified catalog. Fully additive — existing hand-authoredargsTemplatebodies keep working unchanged. See Custom Actions API: Credential sources for which credential sources are self-serve today (SESSION_OMNICHAT/SESSION_AISCRMvia WebbyX One sign-in) versus Wetel-provisioned only (OMNICHAT/NORTIA/AISCRM). -
Added: a new
sub_agentworkflow 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 intoctx[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 thatresultVaris always an array, even for a single target, so{{resultVar}}on its own renders[object Object]— index a specific entry ({{resultVar.[0].text}}) instead.
2026-08-28
Section titled “2026-08-28”- Docs fix (read this if any
llmworkflow node sets bothsystemPromptandpromptTemplate): LLM Node’ssystemPromptfield description was wrong, and this page’s own worked example was broken as a result — copying it produced a node that silently never receives itspromptTemplatedata. WhensystemPromptis set, the renderedpromptTemplateis 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: leavesystemPromptunset in almost every case — put your instructions and any{{ctxVar}}data injection together inpromptTemplatealone. The page’s worked example is corrected to match.
2026-08-27
Section titled “2026-08-27”- Added:
createCustomActionnow supports Using Lark — send messages, create/query Base records, route approvals, and more, using your OWN Lark app (not a shared credential). SupplylarkAppId/larkAppSecret/larkRegioninstead ofbearerToken(mutually exclusive with it); Wetel exchanges them for a Larktenant_access_tokenand keeps it refreshed automatically. No new node type — this is aCustom Action/ACTIONnode integration like any other. - Added: New LLM API page —
generateText(single-response) andstartTextGeneration+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 asembed.textGenerationEventsis the first subscription in this API to authenticate viaX-Api-KeyinconnectionParamsinstead 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
BadRequestExceptionnaming 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
textGenerationEventswith 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 thegraphql-wsgotcha behind it: the client’s defaultlazy: truemeansconnectednever fires until something callssubscribe()first, which deadlocks a naive “wait for connected” pattern — passlazy: falseinstead. - Fixed (behavior change — read this if you edit a published workflow via the API): Editing an already-published workflow via
updateWorkflowno longer takes effect on a live session automatically. Previously,updateWorkflowwrote 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. NowupdateWorkflowalways writes to a separate draft; a live session only ever runs whatever was captured by the most recentpublishWorkflowcall. If your integration edits an already-published workflow and expects the change live without a follow-uppublishWorkflowcall, add that call now — see Workflows API: the two-call create pattern for the corrected, now-accurate-in-all-cases description. Theworkflowquery’s ownnodes/edgesfields were also clarified to state plainly that they reflect the draft, not necessarily what’s live. - Added:
sendMessageaccepts a newtestDraft: Booleaninput field — whentrue, 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, unlikedeleteKnowledgeBase, 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 fullrawText, kept separate from theknowledgeDocumentslist query so listing a KB never pays for fetching every document’s text), andupdateKnowledgeDocumentText(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.
2026-08-26
Section titled “2026-08-26”- Added:
sdkVerifyWebbyxOneIdentitynow accepts an optionalproductTicketsarray and returns a newlinkedProductsresult 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: omitproductTicketsand 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 OneX-Client-Idfor every sibling product a signed-in identity can link via theproductTicketsfield above. Lets a sign-in widget discover which extra/sso/logincalls to make without hardcoding a product list. Accepts either a dashboard JWT or the avatarToken fromsdkStart.
2026-08-25
Section titled “2026-08-25”- Added: Custom Actions now support
extraHeadersoncreateCustomAction/updateCustomAction, returned onCustomActionDto— a plain{ [key: string]: string }map of additional static headers merged onto every call made by anACTIONworkflow node, alongside (and never overriding) the existingAuthorizationheader. Unblocks target APIs that require more than one static header, such as a partner requiring bothAuthorizationand a separatex-organization-ididentifier header. Not a secret-storage mechanism — it’s stored and returned as plain JSON, visible to anyone who can querycustomActionsfor your tenant; genuinely secret values still belong inbearerToken, the one credential a custom action supports. See Custom Actions API: Extra static headers and Action Node.
2026-08-24
Section titled “2026-08-24”- Docs: Workflow Best Practices now documents the
_{outputVar}Structured/_{outputVar}StructuredJsoncontext keys atoolnode writes alongside its plain-textoutputVar— populated from the MCP protocol’sstructuredContentfield, or auto-parsed as a fallback if your server returns JSON-shaped text instead. Also documents two supportedresponsenode design choices for consuming it: extracting just a human-readable field for a plain-text client, or hand-authoring a structured JSON envelope (withjsonString) 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.
2026-08-23
Section titled “2026-08-23”- 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 callsPOST /sso/logindirectly 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’srouter/conditionnodes 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.
2026-08-21
Section titled “2026-08-21”- Changed: Bedrock Mantle is now the platform default LLM provider (previously opt-in only via
llmProviderOverride: BEDROCK_MANTLE, as noted below). If you never setllmProviderOverrideon 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, setllmProviderOverrideexplicitly — see Agents: LLM provider override. - Added:
llmworkflow nodes now supportmatchAnyOf— set it to the full list of valid labels on a classification node feeding arouter/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’sdefaultTargetwith no error anywhere. See LLM Node:matchAnyOf. - Fixed:
deactivateChannelConnectornow 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, callupdateChannelConnectorCredentialsagain first — a bareactivateChannelConnectoron a connector with noexternalIdentifiernow fails clearly instead of assuming the old registration still holds.
2026-08-20
Section titled “2026-08-20”- Added:
sendMessage/sdkSendMessagenow accept an optionalclientTurnIdstring, echoed back verbatim on theAiResponseEventproduced 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:sdkSendMessageand Events & Subscriptions. - Added (pilot): Agents and tenants can now override which language model provider answers conversation turns, via
llmProviderOverride(BEDROCK_MANTLE,WEBBYXAI_HAIKU, orGEMINI_DIRECT) and, forBEDROCK_MANTLEspecifically,llmModelOverrideto 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.
2026-08-19
Section titled “2026-08-19”- Added: Channel Connectors — a packaged, zero-code way to connect an Agent directly to Telegram. Five new operations:
createChannelConnector,updateChannelConnectorCredentials,activateChannelConnector,deactivateChannelConnector, andlistChannelConnectors. Activation runs a live health check against the platform and automatically registers the webhook for you — no manual setup step.WhatsApp,Slack, andLarkare validChannelTypevalues 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):
registernow 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 (myTenantreturningnull). If your integration was creating or assigning a tenant itself immediately after callingregister, checkmyTenantfirst — one already exists by the timeregisterreturns, and creating a second is unnecessary.loginalso self-heals this for any pre-existing account that was created before this change and never got a tenant assigned.
2026-08-17
Section titled “2026-08-17”- Added:
ExportRecordDto.statusnow has aFALLBACK_READYvalue, alongside the existingPENDING/RENDERING/READY/FAILED. When arequestStudyKitExportrender fails (including timing out) and a fallback document is configured server-side, the export settles intoFALLBACK_READYinstead ofFAILED—downloadUrlis populated with a fresh presigned URL to a fixed substitute document (7-day expiry, vs. the normalREADYpath’s 24h), anderrorMessageis also populated so you can tell this happened. TreatFALLBACK_READYas a terminal, download-available state alongsideREADYwhen pollingexportRecord. See Evaluation & Export: TheExportRecordDtotype. - Added:
llmworkflow nodes now supportexcludeHistory— settrueon a narrow “extract one value from the current message” node to exclude it from receiving the session’sconversationHistoryin its underlying model call. Fixes a real reliability gap: once a session has a few tool-backed turns behind it, structured JSONresponseenvelopes 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:
llmworkflow nodes withknowledgeBaseIdset now supportallowGeneralKnowledgeFallback— 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
jsonStringHandlebars helper, usable inside any text-field template (aresponsenode’smessageTemplate, anllmnode’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
openingGreetingset and aworkflowIdattached could produce a double greeting on session start — the static greeting racing against the workflow’s own first response.openingGreetingis 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.
2026-08-13
Section titled “2026-08-13”- Corrected (docs): Events & Subscriptions had the
sessionEventssubscription’s$sessionIdvariable typed asInt!in its own example — wrong, it’sID!. Every other session-related field in the API (SdkSendMessageInput.sessionId, etc.) genuinely isInt!, which makesInt!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 alwaysID!; 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_APIis 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
sdkSendMessageonly returns an ack.
2026-08-12
Section titled “2026-08-12”- 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
sessionEventssubscription 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.
2026-08-11
Section titled “2026-08-11”- Added: Agents can now optionally use a hosted photorealistic avatar backend via the
avatarBackendconfiguration field, as an alternative to the default client-side 3D avatar. SetavatarBackend: HOSTED_APIwhen 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_APIavatar 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) andsendText(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
ROUTERnode keyed on turn number. No API surface changed; if you build workflows with turn-count-based routing and useinterruptSession, this is now safe.
2026-08-10
Section titled “2026-08-10”- Added:
AiResponseEventnow has aturnCompletefield, alongside the existingisFinal. If your agent is workflow-driven and can speak multiple lines in one turn (e.g. a workflow with tworesponsenodes in sequence), you previously had no way to distinguish “this line is done” from “the whole turn is done” — every line arrived withisFinal: trueon its own. WatchturnCompleteinstead ofisFinalif you need to know when the agent has genuinely finished replying. See Events & Subscriptions for the full explanation.isFinalis 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 ownclientExternalId/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-uptokensIn/tokensOut/costUsdacross every Session for one customer. Same page linked above. - Added:
MessageDtonow hastokensIn,tokensOut, andcostUsdfields, populated per message (0 forUSERmessages and for workflowresponsenodes 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 onegenerateApiKeykey across every integration. Revoking one no longer affects any other. Fully additive — your existinggenerateApiKeykey keeps working unchanged if you don’t use this. See Authentication: Named API keys.
2026-08-09
Section titled “2026-08-09”- Fixed:
interruptSessionnow accepts either a dashboard JWT or the SDK avatarToken fromsdkStart— 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 againstinterruptSessionusing the avatarToken and seen anUnauthorizederror, this is now fixed. - Fixed: the
sessionEventssubscription’s$sessionIdvariable type isID!, notInt!— 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
conditionnode with no matchingtrue/falseoutgoing edge now fails the run with a clear error, instead of silently stopping with no signal at all. - Fixed:
publishWorkflownow 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:
webhooknodes now support a configurabletimeoutMs, matchingtool/actionnodes. - 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
outputVarcan 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.
Earlier
Section titled “Earlier”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.