Deploy and operate

Observability

See what a Hubzoid deployment is doing through usage and cost, tool decisions, runs, alerts, health checks, OpenTelemetry traces and logs.

Observability in Hubzoid is the record a deployment keeps of itself: usage for every model call, every restricted tool decision, every access change and sign-in, every run, and every webhook event and alert. Settings and the command line read those records directly from the deployment's own databases, while alerts and traces to an outside backend are optional and off until you set them up.

When to use it

Use this page to set up monitoring for a deployment, to answer "who used what and what did it cost", and to investigate a refused tool call or a failed run.

QuestionWhere to look
Is the deployment up?GET /healthz and GET /healthz/workflows
Is it configured safely, and backed up?hubzoid doctor <hub>
What did each agent cost this month?Settings → Usage
Who called which restricted tool?hubzoid audit, or Settings → Activity → Audit log → Tool decisions
Who changed access, and who signed in?Activity → Audit log → Access changes
Did last night's workflow run?Activity → Runs, or hubzoid schedule status <hub>

The smallest real example

hubzoid doctor reads a Hub and its deployment without changing anything. These two lines are from a starter Hub:

Output
✓ Last backup 0.0 days ago. Scheduled backups run weekly. (backup.age)
✓ Scheduled work is not held or paused (scheduler.health)

Each check has a stable id in brackets. Run it with --json from a monitoring job and alert on the ids that matter to you.

How it works

Everything below lives in the deployment's own databases. Run history stays in the workflow engine's database, and Hubzoid keeps no second copy.

RecordContentsRead it with
UsageTime, Hub, surface, user, chat, model, tokens, estimated cost, status and duration. No message content.Usage in Settings
Tool decisionsTime, Hub, user, surface, tool, decision and reason for every restricted tool callhubzoid audit, Activity → Audit log
Access changesGrants and revokes, accounts added, approved or deleted, sign-ins and sign-outs, two-step sign-in changes, and workflow run, pause, resume and cancel actionsActivity → Audit log
RunsEvery scheduled, manual, webhook and chat-started run with its steps, result and errorActivity → Runs, each agent's Workflows tab, hubzoid schedule status
Webhook events and alertsEach received webhook event and each alert delivery, with its statehubzoid schedule deliveries

Webhook event and alert records have no automatic retention yet.

Usage and cost

Settings → Usage shows five totals for the last 24 hours, 7 days or 30 days, and this month's use by agent with its spending limit.

Settings, Usage over 30 days: Messages 1, Users 1, Tokens used 80.2K, Workflow runs 1 with 0 failed, Approx. cost $0.07, and below it My Hub with this month's output tokens, cost, No monthly limit and Set limits.Settings, Usage over 30 days: Messages 1, Users 1, Tokens used 80.2K, Workflow runs 1 with 0 failed, Approx. cost $0.07, and below it My Hub with this month's output tokens, cost, No monthly limit and Set limits.
Totals for the period, then each agent's use this month against its limit.
NumberMeaning
MessagesChat messages on every chat surface, including Slack, WhatsApp and Telegram, with the number of conversations. Background title and suggestion calls are not counted.
UsersSign-in accounts in the viewer's scope.
Tokens usedChat turns plus background calls, scheduled work and workflow model calls, split into input and output.
Workflow runsRuns started in the period, with how many failed.
Approx. costIn USD, from the model's reported cost or token prices. Calls with no known price are left out and marked with *.

A figure Hubzoid cannot know reads Unavailable or a dash, never zero. The Agents list also shows each agent's chats and spend, and agent administrators see only their own agents.

Tool decisions

Every call to a restricted tool is checked in code and written as one row, allowed or denied. A call whose row cannot be written is refused, so every call that ran is in the log.

Terminal
hubzoid audit ./finance --denied
Every audit option
OptionDefaultEffect
--limit, -n50How many recent decisions to show
--usernoneOnly one person or service identity
--deniedoffOnly refused calls
--jsonoffOnly JSON: {"entries": [{ts, user, decision, tool, surface, reason}]}

Each line shows the time, the user, ALLOW or DENY, the tool, and the surface with a reason.

Every decision reason
ReasonMeaning
grantAllowed by a grant
no-grantThe caller lacks the permission
anonymousNo verified identity reached the bridge
surface:<name>The surface may not reach restricted tools, for example surface:slack-channel
blockedThe person's agent access is blocked
store-errorThe access store could not be read, so the call was denied

Rows written by older releases can also carry group and no-group, from the group-based access those releases had.

Settings, Activity, Audit log tab with Access changes selected: sign-ins, Sam Rivera turning on two-step sign-in, and a server operator allowing people to use My Hub, with filters for agent, time, affected person, changed by and action.Settings, Activity, Audit log tab with Access changes selected: sign-ins, Sam Rivera turning on two-step sign-in, and a server operator allowing people to use My Hub, with filters for agent, time, affected person, changed by and action.
Access changes and Tool decisions sit side by side under Activity.

Activity → Audit log shows the same decisions under Tool decisions, and every grant, revoke, account change and sign-in event under Access changes, with filters for agent, time, person and action. Filters live in the URL, so a filtered view can be shared. See people and activity.

Scheduled runs

Activity → Runs lists every run with its status, how it started and how long it took. Each agent's Workflows tab shows its workflows, whether each is on, who it runs as, and a link to its runs. A run's result and step outputs are shown only to the account the run acted as, and other managers see a failure summary. On the server:

Terminal
hubzoid schedule status ./finance          # definitions, recent runs and errors
hubzoid schedule deliveries ./finance      # webhook events and alert deliveries

hubzoid doctor reports scheduler.health: a hold set by a backup in progress, paused work, a failed dispatcher, or no recent dispatcher heartbeat. See operating runs.

Workflow alerts

Hubzoid can tell you when scheduled work goes wrong: failed runs (for webhook runs, only after their retries run out), failed scheduled eval suites, overdue runs, several failures in a row, a schedule that stopped firing, webhook events whose content changed under the same key, and a workflow engine that stopped responding. Repeats within an hour are combined into one alert with a count.

my-hub/workflows/settings.yaml
alerts:
  to:
    - webhook: ALERT_WEBHOOK_URL # the name of a variable that holds the URL
    - slack: SLACK_ALERT_WEBHOOK # a Slack incoming webhook URL, also by variable name
    - email: ops@example.com
  cooldown: 1h
  failures_in_a_row: 3
  pause_after_failures: 20
Every alert setting
SettingEffect
alerts.toThe Hub's destinations. webhook and slack name an environment variable that holds the URL, and email is an address, sent through the deployment's SMTP settings.
alert_to= on a workflow, alert_to: in a markdown taskDestinations for that one workflow or task, instead of the Hub's
HUBZOID_ALERT_URLThe deployment's fallback webhook URL
HUBZOID_ALERT_SECRETSigns each webhook alert with X-Hubzoid-Signature, an HMAC-SHA256 of the timestamp, a . and the raw body
failures_in_a_rowHow many failures in a row raise an alert. Default 3.
pause_after_failuresA schedule pauses itself after this many failed scheduled runs in a row. Default 20, and 0 turns it off. Webhook workflows are never paused.
Delivery, signing, and what an alert carries

Delivery is durable and retried up to five times. Webhook alerts carry an Idempotency-Key and an X-Hubzoid-Timestamp header. Every alert carries only the Hub, the kind of alert, the workflow, the run id, a count and a link to the run in Settings. Outputs and exception text stay on the server. With HUBZOID_EMAIL_DELIVERY=preview, email alerts are written to private files under .hubzoid/alert-preview/ instead of being sent. A missing destination or an exhausted delivery shows in hubzoid schedule deliveries, never as a successful send.

A Hub's first start with alerts looks back 24 hours, so older failures are never alerted. An invalid workflows/settings.yaml turns workflows off for that Hub, with the error in its workflow health, while chat and other Hubs keep running. See code workflows.

Health checks

CheckWhereUse it for
GET /healthzThe public port, answered by the Hub's bridge (the first Hub's when several run together)Liveness probes. Returns status, hub, agent and model. The Docker image uses it as its health check.
GET /healthz/workflowsThe public port, answered by the edgeUptime monitors. 200 when every Hub's workflow engine is healthy, 503 otherwise. It names no Hub and no error.
hubzoid doctor <hub> --jsonThe serverConfiguration and state checks for scripts and monitoring

Watch /healthz/workflows from outside the machine, and supervise the processes as well, because an edge cannot report the death of its own host. The edge also sends an alert when a Hub's workflow engine goes stale and when it recovers.

Doctor reads only. It creates no database, runs no migration and starts no engine, so it is safe against a running deployment. It exits with status 1 when any check fails.

Terminal
hubzoid doctor /opt/hubzoid/agents/finance --json
Output
{
  "format": 1,
  "hub": "/opt/hubzoid/agents/finance",
  "hubzoid": "<version>",
  "ok": true,
  "checks": [
    {
      "id": "auth.bridge_keys",
      "status": "ok",
      "summary": "1 bridge key(s) set",
      "detail": null
    },
    {
      "id": "backup.age",
      "status": "warn",
      "summary": "No backup recorded. Scheduled backups run weekly. Run `hubzoid backup` now.",
      "detail": null
    }
  ]
}

Every check has a stable id and a status of ok, info, warn or fail. New checks may be added, and existing ids are never renamed.

Doctor checks worth monitoring
CheckWatches
db.operational, db.hubDatabases reachable and at the current schema
scheduler.healthScheduled work held, paused, failing or without a heartbeat
backup.ageThe last verified backup, a failed one, or none in 7 days
auth.bridge_keys, auth.chat_signin, exposure.bind, exposure.local_modeBridge keys set, sign-in on, and how the public port is bound
deployment.keyThe deployment key, by fingerprint, and whether its file is readable by others
access.whoAn agent nobody may use yet
model.credentialsProvider credentials present for the configured model
deps.sqliteSQLite new enough for the workflow engine

The doctor reference lists every check.

OpenTelemetry traces

Hubzoid can push traces over OTLP/HTTP to any OpenTelemetry backend, such as Langfuse or an OpenTelemetry Collector. It is off until you set an endpoint in the Hub's environment:

my-hub/.env
HUBZOID_OTEL_ENDPOINT=https://langfuse.internal/api/public/otel
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic <base64 of public_key:secret_key>

OTEL_EXPORTER_OTLP_HEADERS is the standard exporter variable and passes straight through. Emission is an outbound push, and nothing is added to the public port, so keep the receiver on a private network. When several Hubs run together, set HUBZOID_OTEL_ENDPOINT in each Hub's .env, pointing at the same backend. Traces separate by hubzoid.hub.

The spans come from the model SDKs, and what they carry depends on the model path.

What each model path emits, and Langfuse cost
Model pathWhat is emittedAttribution
LiteLLM providers (OpenRouter, OpenAI, Azure, Anthropic and others)LiteLLM's OpenTelemetry callback, with standard gen_ai.usage.* token attributes and gen_ai.cost.total_costhubzoid.hub as a resource attribute
claude-localClaude Code's trace spans (interaction, llm_request, tool) with token countshubzoid.user, hubzoid.hub, hubzoid.surface and user.id (the signed-in person) as resource attributes

On the claude-local path only traces are exported by default. Metrics and logs stay off, because logs can carry prompt content. Set OTEL_METRICS_EXPORTER=otlp or OTEL_LOGS_EXPORTER=otlp to send them, and OTEL_EXPORTER_OTLP_PROTOCOL=grpc for a gRPC-only collector.

Claude Code puts token counts under names Langfuse does not map, so cost shows as zero there. Set HUBZOID_OTEL_NORMALIZE=true to fix this inside the bridge, with no extra process: it copies the token counts to the gen_ai.usage.* names, sets each span's user.id to the signed-in person, adds the Hub name as a Langfuse tag so cost can be charted by Hub, and forwards the batch. If you already run a shared collector, point HUBZOID_OTEL_ENDPOINT at it, leave normalization off, and use the reference docs/otel-collector.yaml (opens in a new tab). Langfuse prices tokens from its own model table, so a model it does not know shows zero cost until you add its price there.

Message content

On the claude-local path, prompt and response bodies stay out of spans unless you set OTEL_LOG_USER_PROMPTS=1 or OTEL_LOG_ASSISTANT_RESPONSES=1. On the LiteLLM path, spans are produced by LiteLLM's callback. Review what your backend receives before pointing a production Hub at a shared or third-party backend.

OpenAI tracing and eval traces

Two related switches are separate. HUBZOID_OPENAI_TRACING=true lets the OpenAI Agents SDK export its own traces, including prompts and tool data, to OpenAI. It is off by default. Eval runs push one trace per case with scores to Langfuse when LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set (or read back from the Basic credentials in OTEL_EXPORTER_OTLP_HEADERS), tagged hubzoid.eval so you can filter them out. See evals.

Logs

SourceWhere
A Hub, with its channelshubzoid logs <hub> -f, and the process's standard output
The edgehubzoid logs edge -f
The whole deployment under systemdjournalctl -u hubzoid -f

HUB_LOG_LEVEL sets the level for the bridge and edge: info (default), debug or warning. Hubzoid removes one-time sign-in link tokens and OAuth codes from its own access logs, but a reverse proxy in front of it still records them unless you redact them there.

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.