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:
| Field | Type | Notes |
|---|---|---|
id | Int! | |
workflowId | Int! | |
sessionId | Int! | |
agentId | Int! | |
status | WorkflowRunStatus! | Run-level status enum. |
nodeTrace | JSON | Ordered, per-node execution trace — see below. |
totalTokensUsed | Int! | LLM tokens consumed by this run specifically. |
errorMessage | String | Populated only if the run failed. |
context | JSON | The workflow’s variable context at the point the run stopped. |
createdAt | DateTime | |
completedAt | DateTime |
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 /graphqlContent-Type: application/jsonAuthorization: Bearer <jwt>x-huat-platform: customernodeTrace 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 timesinterruptSessionfired 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 alongsidetotalTokensUsedon 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 EvaluationDto — score (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 /graphqlContent-Type: application/jsonAuthorization: Bearer <jwt>x-huat-platform: customerImmediately 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.
Putting the three levels together
Section titled “Putting the three levels together”A practical pattern for building your own observability tooling:
- While live: subscribe to
sessionEvents, captureworkflowRunIdvalues as they appear, and renderNodeExecutingEvent/NodeFailedEventas process indicators in your own UI. - Right after a session ends: if something looked off (a
NodeFailedEventfired, orinterruptCountlooks high), pullworkflowRun(id)for the captured run id and readnodeTraceto see exactly which node did what. - A few seconds later: poll
evaluation(sessionId)for the async quality score, and pair it withsession(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.
Next steps
Section titled “Next steps”- Troubleshooting — step-by-step recipes for specific failure symptoms, including the
workflowRun.nodeTracewalkthrough this page builds on. - Events & Subscriptions — the full real-time event catalogue and connection details for Level 1.
- API Reference: Sessions — the complete
SessionDto/MessageDtofield reference. - API Reference: Evaluation & Export — the complete
EvaluationDtoreference and async-timing details. - Workbench (Apollo Sandbox) — a live reference tool you can point at your own agents while testing these queries.