Technical docs
Logs
Overview
Support and developer reference for execution-log surfaces backed by workflow runs.
Surfaces that use this system
- Org-wide logs page: `/logs`
- AI teammate logs tab: `/staff/ai/[employee_id]?tab=logs`
- AI teammate API tab: `/staff/ai/[employee_id]?tab=api`
All three surfaces reuse the same `AiTeammateLogsTable` + `useWorkflowRuns()` stack and are backed by org-wide workflow-run APIs.
Org-wide `/logs` access
- Frontend route: `/logs`
- Frontend gate: `canAccessTool({ isOrgAdmin: true }, user)`
- If access fails, the page renders `AccessDenied`.
Org-wide page behavior
- Header: `Execution Logs`
- Description: `All skill executions across the organization`
- Scope: workflow runs across all skills and AI teammates in the current organization
- Data source: `useWorkflowRuns()` backed by `GET /api/organizations/{orgId}/workflows/runs/`
- Default page size: `25`
- Allowed page sizes: `10`, `25`, `50`, `100`
- Org-wide `/logs` and teammate `Logs` tab both persist page size in localStorage under `alloy:logs-page-size`
Shared logs-table behavior
- Runs are ordered newest-first by `created_at`.
- Refresh re-fetches the current page.
- Rows can be deep-linked with `?run=<runId>`.
- Pagination state is reflected as `?page=<n>`.
- Filter and search state is synced into the URL and preserves unrelated params such as `run` and `tab`.
- Expanding a row lazy-loads full run details from:
- `GET /api/organizations/{orgId}/workflows/{workflowId}/runs/{runId}/`
- Expanded details can show:
- run metadata
- runtime context
- copied run links and IDs
- conversation ID copy action
- trigger URL copy action when `trigger_url` exists
- flow/config snapshot
- parsed steps
- tool call args/results
- voice transcripts and attachments when present
- If an expanded run is no longer visible after filtering/pagination, the UI collapses it.
Search behavior
- UUID search is server-backed exact match.
- Plain-text search is not sent to the backend.
- Search flow today:
- UUID input searches `conversation_id` first
- if that returns zero results once, the UI falls back to `run_id`
- pasting a URL containing `run=<uuid>` switches immediately into run-ID search
- non-UUID text search is client-side against the currently loaded rows only
- Current client-side text search matches:
- skill name
- input message
- output message
- error message
- model
- run ID
- AI teammate name
- conversation channel label/key
Filters
- Pagination and API filters are server-side.
- UI chip filters are single-select because the server API accepts one value per field.
- Org-wide `/logs` shows filter sections:
- `Status`
- `Channel`
- `Model`
- `Skill`
- `AI Teammate`
- `Date Range`
- `Duration`
- `Tokens`
- AI teammate `Logs` tab reuses the same filters but fixes `ai_employee_id` from page context and hides the teammate column.
- AI teammate `API` tab hides the channel filter and then client-filters the current page to `API`-labeled rows.
- Date presets:
- `Last hour`
- `Last 24h`
- `Last 7 days`
- `Last 30 days`
- Duration presets:
- `< 1s`
- `1–10s`
- `10–60s`
- `> 1 min`
- Token presets:
- `< 100`
- `100–1K`
- `1K–10K`
- `> 10K`
Source labels
- UI channel labels are derived as:
- `employee_chat` -> `Internal Chat`
- `ally_chat` -> `Ally Chat`
- `web_chat` -> `Web Chat`
- `skill_chat` -> `Skill Chat`
- no conversation -> `API`
- When the backend already populated `conversation_channel` or top-level `channel`, the UI uses that value first.
- Otherwise the UI derives:
- `API` when there is no `conversation_id`
- `Web Chat` when `runtimeContext.isAsync === true`
- `Internal Chat` as the fallback for conversation-backed runs without a more specific channel
Backend/API behavior
- List endpoint:
- `GET /api/organizations/{orgId}/workflows/runs/`
- Detail endpoint:
- `GET /api/organizations/{orgId}/workflows/{workflowId}/runs/{runId}/`
- Both routes use `checkOrganizationAccess`.
- List query validation currently supports:
- pagination: `page`, `limit` (`limit` max `100`)
- exact filters: `conversation_id`, `status`, `run_id`, `channel`, `ai_employee_id`, `workflow_id`, `model`, `trigger_id`
- runtime-state filters: `state[{key}]={value}` exact matches against `state.runtimeContext.state`
- range filters: `started`, `ended`, `duration`, `tokens`
- `exclude` limited to `flow`, `state`, or `flow,state`
- The frontend list surfaces send `exclude=flow,state`.
- Current backend behavior for any truthy `exclude`:
- switches serialization to `WorkflowRun.listData()`
- excludes only `flow` at the DB query layer
- still returns a compressed `state`
- Compressed list `state` includes:
- `currentStepId`
- top-level `error`
- `runtimeContext.userId`
- `runtimeContext.isAsync`
- trimmed step summaries with reduced input/output payloads
- List responses are also enriched with aggregated `costs[]` entries when token-history rows exist for the run.
- Each `costs[]` item is split by token `key_type` and currently includes:
- `key_type`
- `input_tokens`
- `output_tokens`
- `cached_input_tokens`
- `usd_cost`
- Detail endpoint behavior:
- `404` if the run does not exist
- `404` if the run exists but does not belong to the requested workflow
- loads up to `1000` step logs
- backfills temporary conversation titles when the run has a conversation ID but no stored title
Current limitations that matter for support
- Plain-text search only searches the rows already loaded on the current page.
- `started` filter maps to run `created_at`.
- `ended` filter maps to `completed_at` only, not `failed_at` or `suspended_at`.
- The `API` channel label is often derived client-side from the absence of `conversation_id`.
- Backend `channel` is not reliably populated for direct API-triggered runs, so server-side `channel=api` filtering is not dependable even though the UI can label those rows as `API`.
Cross-reference
- The org-wide page is `/logs`.
- Teammate logs and API request history reuse the same run data model.
- General workflow-run internals are documented in `workflow-engine.md`.