Skip to content

Observability & Debugging Your Agent

This content is not available in your language yet.

There’s no visual dashboard that shows this for you today — every observability capability described on this page is something you query yourself, over GraphQL, using your own client (Apollo Sandbox, your backend, or a small script). Think of this page as the map of “what can I ask the API to find out what my agent actually did,” organized into three levels: what’s happening right now, what happened in one specific run, and how a session (or your evaluation history) looked in aggregate.

If you’re debugging one specific broken interaction, Troubleshooting has step-by-step recipes. This page is the broader reference for building your own ongoing visibility into agent behavior — logging, internal tooling, support workflows, whatever you build on top of these primitives.

Level 1: Real-time — while a session is happening

Section titled “Level 1: Real-time — while a session is happening”

The sessionEvents subscription is the live feed: the agent’s streaming text reply, workflow node execution as it happens (NodeExecutingEvent, NodeFailedEvent), and session lifecycle transitions. This is genuinely real-time — there’s no polling involved, and it’s the same channel your production UI should already be consuming to render replies.

This page won’t re-document the event catalogue or the connection handshake — see Events & Subscriptions for the full reference, including the union-fragment gotcha (omit an inline fragment for an event type and it silently never arrives, no error).

For observability purposes specifically, the useful habit is: capture workflowRunId off NodeExecutingEvent/NodeFailedEvent while the session is live and log it to your own store. There is no query that lists past workflow runs — the events subscription is the only place a workflowRunId is ever surfaced, so if you don’t capture it in the moment, you lose the ability to look up that run’s detailed trace afterward.

Level 2: Per-run — inspecting one workflow run’s execution

Section titled “Level 2: Per-run — inspecting one workflow run’s execution”

Once you have a workflowRunId (captured live, per above), workflowRun(id) is the deep debugging surface for “why did the workflow do that.” It returns a WorkflowRunDto:

FieldTypeNotes
idInt!
workflowIdInt!
sessionIdInt!
agentIdInt!
statusWorkflowRunStatus!Run-level status enum.
nodeTraceJSONOrdered, per-node execution trace — see below.
totalTokensUsedInt!LLM tokens consumed by this run specifically.
errorMessageStringPopulated only if the run failed.
contextJSONThe workflow’s variable context at the point the run stopped.
createdAtDateTime
completedAtDateTime

Auth: JWT only. workflowRun/workflowRuns are dashboard-tier queries — they are not reachable with an SDK API key or a session-scoped avatarToken. If you’re building internal tooling for support/ops staff to inspect runs, that tooling needs a real dashboard login, not the embed SDK’s credentials.

query DebugRun($id: Int!) {
workflowRun(id: $id) {
id
status
context
nodeTrace
errorMessage
totalTokensUsed
completedAt
}
}
POST /graphql
Content-Type: application/json
Authorization: Bearer <jwt>
x-huat-platform: customer

nodeTrace is an array with one entry per node the run actually visited, in execution order:

{
nodeId: string
nodeType: string
startedAt: string
finishedAt: string
outputVar?: string
outputValue?: unknown
renderedRequest?: unknown
tokensUsed?: number
routedToDefault?: boolean
retrievedChunkIds?: number[]
retrievalError?: string
status?: 'COMPLETED' | 'FAILED'
error?: string
}

Optional fields are simply absent (not false/null) when they don’t apply to a given node type — retrievedChunkIds only shows up on LLM nodes with a knowledge base attached, for example. Check for presence rather than assuming a default value. This is the same trace shape Troubleshooting uses for debugging a specific failed run; this page is pointing at the same field for the broader case of “understand what any run — failed or not — actually did.”

renderedRequest only appears on tool (MCP), action (Custom Action), and webhook node types — it’s the actual interpolated request body/args Wetel sent to the partner endpoint for that node, captured on both success and failure. Usually an object (the parsed JSON body/args), but if a tenant’s argsTemplate/bodyTemplate failed to render to valid JSON, it comes through as a raw string instead — check which you got before assuming a shape. It’s not retroactive: a trace captured before this field shipped won’t have it, even for the same node type.

renderedRequest now runs through a best-effort secret-redaction pass before it’s stored: JWTs, Bearer <token> headers, vendor key prefixes (sk-, pk-, Slack’s xoxb-/xoxp-, AWS’s AKIA), and long values sitting under sensitive-sounding key names (apiKey, token, secret, password, authorization, credential) are all redacted. This is defense-in-depth, not a guarantee — a secret with no recognizable shape (e.g. a bare opaque string under an innocuous key like value), or one embedded mid-string rather than as a whole field value, can still come through unredacted. The real fix is to never hardcode a secret into argsTemplate/bodyTemplate in the first place — use a Custom Action’s own credential-source mechanism instead, which resolves the secret into request headers server-side and keeps it out of the rendered body (and therefore this trace field) entirely.

On a tool/action/webhook node whose partner endpoint returns a non-2xx response, error now includes the partner’s own response body (status code, and its JSON message/error/errors field or, for a non-JSON body, up to ~500 characters of raw text) alongside the generic HTTP failure message — not just "Request failed with status code 400" on its own. This is the actual reason a partner rejected a request (a validation message, a missing-field error, etc.), previously invisible from Wetel’s side. Bounded and redacted the same way as renderedRequest. Not retroactive — a trace captured before this shipped only has the generic message.

If you want to see every run tied to a given session rather than one specific run by id, use workflowRuns(sessionId: Int!): [WorkflowRunDto!]! — useful when a single session triggered the workflow more than once (e.g. a loop, or multiple turns each re-entering the graph).

query RunsForSession($sessionId: Int!) {
workflowRuns(sessionId: $sessionId) {
id
status
totalTokensUsed
errorMessage
createdAt
completedAt
}
}

There’s still no query that lists runs across an entire agent or tenant without a sessionId — this is a session-scoped lookup, not a global run browser.

Level 3: Per-session and aggregate — what happened over the whole conversation

Section titled “Level 3: Per-session and aggregate — what happened over the whole conversation”

Two separate things live at this level: cheap, synchronous session-level counters, and a richer async evaluation that’s generated after the session ends.

Session-level counters (synchronous, always available)

Section titled “Session-level counters (synchronous, always available)”

SessionDto carries two fields worth watching for basic session health, on top of the full field set documented in API Reference: Sessions:

  • interruptCount (Int!) — how many times interruptSession fired for this session. A high count on a single session can indicate the agent is talking over the user, running too long per turn, or a voice-latency problem — worth flagging for review even without listening to the audio.
  • totalTokensInContext (Int!) — the running LLM context-window token count for the session. Useful for spotting sessions approaching context limits, or for rough cost attribution alongside totalTokensUsed on individual workflow runs.

Both are available synchronously via session(id) — no polling needed, unlike the evaluation below:

query SessionHealth($id: ID!) {
session(id: $id) {
id
status
interruptCount
totalTokensInContext
lastEventAt
messages {
role
text
turnIndex
}
}
}

messages (the full chat history) is also worth pulling into the same query when you’re reviewing a specific conversation — there’s no separate chat-history endpoint; it’s always fetched nested under session, as covered in API Reference: Sessions.

Evaluation — async quality scoring (poll after the session ends)

Section titled “Evaluation — async quality scoring (poll after the session ends)”

evaluation(sessionId) is the closest thing Wetel has to an automated QA layer today: after a session ends, a background job scores the conversation and returns a structured EvaluationDtoscore (Int), recommendation, strengths, weaknesses, summary, and the transcript it was generated from.

This is genuinely asynchronous — it does not exist yet the instant a session ends, and there is currently no push notification when it finishes. You have to poll:

query GetEvaluation($sessionId: Int!) {
evaluation(sessionId: $sessionId) {
id
score
recommendation
strengths
weaknesses
summary
generatedAt
}
}
POST /graphql
Content-Type: application/json
Authorization: Bearer <jwt>
x-huat-platform: customer

Immediately after endSession/sdkEndSession, expect evaluation(sessionId) to return null — that’s expected, not a bug. Wait a few seconds and re-query, or build a light polling loop, until generatedAt is populated. The full field reference, including why evaluation only runs for conversations with at least two turns, lives in API Reference: Evaluation & Export — read that page for the exact async-timing caveats before building anything that depends on this field.

Used across many sessions, score/recommendation/strengths/weaknesses is the closest thing to an aggregate quality signal available today — there’s no built-in aggregation query (no “average score this week” endpoint), so building a trend view means pulling individual evaluation records yourself and aggregating client-side.

A practical pattern for building your own observability tooling:

  1. While live: subscribe to sessionEvents, capture workflowRunId values as they appear, and render NodeExecutingEvent/NodeFailedEvent as process indicators in your own UI.
  2. Right after a session ends: if something looked off (a NodeFailedEvent fired, or interruptCount looks high), pull workflowRun(id) for the captured run id and read nodeTrace to see exactly which node did what.
  3. A few seconds later: poll evaluation(sessionId) for the async quality score, and pair it with session(id) { messages } if you need the full transcript alongside the score.

None of this requires a hosted dashboard screen — every step above is a GraphQL call you make yourself, from your own backend or Apollo Sandbox.