---
title: "List Worker Events"
method: GET
path: "/worker/{worker_id}/events"
tags: ["workers"]
---

# List Worker Events

`GET /worker/{worker_id}/events`

## Request

- Base URL: `https://api.traversal.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.traversal.com/worker/{worker_id}/events`
- Auth: none declared

A window of this worker's merged event feed, newest-first.

Pass an empty ``seek`` object for the newest page.
``around_source`` + ``around_id`` centre the window on one event, which is
what a link to a message resolves through. They are a source and an id
rather than a position: the ordering key is not a stable identifier, so a
link built from one would break the moment ordering changed. An anchor
that cannot be reached returns the newest page with ``anchor_found``
false, never an error and never a scan. An around request addresses the
unfiltered feed and ignores ``search``.

## Path parameters

- `worker_id` string, required

## Query parameters

- `seek` WorkerEventsSeekParams, required: Validated query parameters selecting at most one feed seek mode.
  - `before_occurred_at` string, date-time, nullable: Older-direction cursor timestamp
  - `before_source` 'resident_agent_event' | 'transcript_summary_events' | 'chatbot_messages': Feed-layer discriminator for which table a feed row came from. This is a feed concept, distinct from the cursor-stream ``source`` protocol: the feed renders persisted rows (including non-streamable ``chatbot_messages``), it does not tail cursor streams. These values are the canonical vocabulary bound into ``worker_feed_dao._FEED_QUERY``.
  - `before_id` string, nullable: Older-direction cursor tie-break id
  - `after_occurred_at` string, date-time, nullable: Newer-direction cursor timestamp
  - `after_source` 'resident_agent_event' | 'transcript_summary_events' | 'chatbot_messages': Feed-layer discriminator for which table a feed row came from. This is a feed concept, distinct from the cursor-stream ``source`` protocol: the feed renders persisted rows (including non-streamable ``chatbot_messages``), it does not tail cursor streams. These values are the canonical vocabulary bound into ``worker_feed_dao._FEED_QUERY``.
  - `after_id` string, nullable: Newer-direction cursor tie-break id
  - `around_source` 'resident_agent_event' | 'transcript_summary_events' | 'chatbot_messages': Feed-layer discriminator for which table a feed row came from. This is a feed concept, distinct from the cursor-stream ``source`` protocol: the feed renders persisted rows (including non-streamable ``chatbot_messages``), it does not tail cursor streams. These values are the canonical vocabulary bound into ``worker_feed_dao._FEED_QUERY``.
  - `around_id` string, nullable: Id of the event to centre the window on
- `limit` integer: Number of events per page
- `search` string, nullable: Case-insensitive chat search. Free text matches message text; `from:name` filters by sender. Queries under 3 characters return no results. Ignored when around_source and around_id are set: an around request addresses the unfiltered feed.
- `thread_id` string, nullable: Scope the window to one chat thread (origin + replies). Off-window origins are attached as presentation=thread_origin.

## Response `200`

Successful Response

- ListWorkerEventsResponse: One window of the feed, newest-first, with an edge on each side. ``has_more`` / ``next_cursor`` describe the older edge and ``has_more_newer`` / ``prev_cursor`` the newer one. A request only reports the edges it actually looked at: paging older leaves the newer edge false and null, paging newer leaves the older edge false and null, and an around window reports both because it scans both ways from its anchor. ``limit`` bounds the keyed page. Off-window thread origins are appended after that page as ``presentation=thread_origin`` and can make ``events`` longer than ``limit``. Those extras are newest-first among themselves and do not move the cursors.
  - `events` union[], required
    - union
      - ResidentAgentEventItem
        - `source` 'resident_agent_event'
        - `id` string, required
        - `worker_id` string, required
        - `occurred_at` string, date-time, required
        - `event_type` string, required
        - `data` union, required
          - ToolAction: A synchronous tool call the agent made — its rendered ``tool_response`` stored verbatim because the renderer must reproduce it byte-identically. Covers every tool that returns a tool_response synchronously: the platform's send tool / ``react_to_slack`` / ``private_feedback``, plus the defensive case where the LLM emits a name outside the registered set (the workflow synthesizes a ``"Tool 'X' does not exist..."`` error response). Differentiated by ``tool_name`` — no need for per-tool variants because the data is identical. Stays separate from ``InvestigationCall`` (async result) and ``WaitEntry`` (response derived from semantic fields).
            - `kind` 'tool_action'
            - `occurred_at` string, date-time, required
            - `tool_call_id` string, required
            - `tool_name` string, required
            - `tool_response` WorkflowMessage, required
              - …
            - `result_ts` string, nullable
            - `request_summary` string, nullable
            - `outbound_message` string, nullable
            - `generated_html_sessions` object
          - InvestigationCall: The agent dispatched an ``investigate`` child workflow. Fire-and-forget and **immutable**: the LLM gets back a ``stub_response`` immediately, and the actual report arrives later as its own ``InvestigationFinished`` event (paired back by ``tool_call_id``). The result is never mutated onto this entry, so the action log is append-only and rebuilds faithfully from the DB.
            - `kind` 'investigation'
            - `occurred_at` string, date-time, required
            - `tool_call_id` string, required
            - `investigation_id` string, required
            - `system_chat_id` string
            - `question` string
            - `investigation_rationale` string
            - `usage_limit_refused` boolean
            - `usage_limits` InvestigationLimitsSnapshot: The finite investigation limits that are one worker's own, as of ``as_of``. Only budgets counted against the worker itself — or, for a private chat, its owner — appear. Budgets shared across the organization are left out, so an empty ``limits`` states that nothing caps this worker's own starts, not that nothing could refuse one.
              - …
            - `agent_profile` 'rca' | 'fix': Which agent a session runs: its tools, prompt, budgets, and how a turn ends. Fixed when the session is created, so every follow-up runs the same agent. Asking a different agent means starting a new session.
            - `stub_response` WorkflowMessage, required
              - …
          - InvestigationFinished: The async result of an ``InvestigationCall`` landed — its own append-only event (never mutated onto the call), so the result persists durably and rebuilds from the DB. Paired back to its call by ``tool_call_id`` — follow-ups reuse the parent's ``investigation_id``, so the call id is the unambiguous key. ``occurred_at`` is the arrival time; readers pair it with its call to render the ``## Investigation finished`` block.
            - `kind` 'investigation_finished'
            - `occurred_at` string, date-time, required
            - `tool_call_id` string, required
            - `investigation_id` string, required
            - `result_user_message` WorkflowMessage, required
              - …
          - WaitEntry: The agent called ``wait``; the workflow paused until new deltas arrived. Started and ended timestamps are kept separately so the renderer can reconstruct the "Waited Xm." tool response from semantic fields alone — no need to store the rendered string. ``timer_expired`` is set when the self-scheduled deadline fired with no other wake reason, mirroring the existing "Timer expired." suffix.
            - `kind` 'wait'
            - `started_at` string, date-time, required
            - `ended_at` string, date-time, required
            - `tool_call_id` string, required
            - `reasoning` string
            - `seconds` integer, nullable
            - `timer_expired` boolean
          - WorkerStatusUpdate: The worker's status top-line, emitted when it declares one on ``wait``. A first-class action-log event so it streams to the /workers UI in real time; the persist path also projects the latest value onto the ``resident_agent_workflow`` row for the cheap list read. Distinct from ``WaitEntry`` (wait timing) — the status is fully known at dispatch and doesn't wait for the wait to end.
            - `kind` 'worker_status_update'
            - `occurred_at` string, date-time, required
            - `status` 'critical' | 'attention' | 'working' | 'stable', required: The worker's present-tense status top-line, shown as a pill on the /workers list. Only these agent-picked values are ever stored in the `status` column; a null column means none has been emitted yet (the UI renders that as `starting`), and the terminal `stopped` state is derived from `stopped_at` — neither is stored here, so there is no second source of truth. The agent-facing definitions live on the ``status`` field in ``prompts.py`` (the only copy the model reads); keep this summary in sync with it. Values: CRITICAL: Immediate coordinated response is warranted because user-facing impact is severe or escalating. ATTENTION: A specific person or team should act now, but immediate coordinated incident response is not warranted. STABLE: Resting floor — no specific person or team needs to act now and no coordinated incident response is warranted. WORKING: Legacy — no longer emittable, see ``EmittableWorkerStatus``.
            - `message` string, nullable
          - SystemReminder: A ``[system reminder]`` line queued by the workflow itself (e.g. the message-budget soft-warning) and drained into the next LLM turn as a user message. Distinct from Slack events because the source is internal, not the channel. The renderer emits these last in each turn's drain bundle so they're the freshest context before the LLM's response.
            - `kind` 'system_reminder'
            - `fired_at` string, date-time, required
            - `text` string, required
          - InstructionChanged: An operator replaced this worker's standing instruction while it was running. Deposited by a signal rather than produced by a turn, and deliberately not a ``SystemReminder``: that kind is hidden from the Workers feed, and an operator editing a worker's directive is something a reader of that feed should see. Rides along to the next drain instead of waking one — like a reaction, the edit is context for whatever the worker does next rather than a reason to act now. The prefix frozen at run start still carries the superseded instruction, which is why the rendered text says so outright. A later roll rebuilds the prefix from the row and the discrepancy resolves itself.
            - `kind` 'instruction_changed'
            - `changed_at` string, date-time, required
            - `instruction` string, nullable, required
          - UsageBudgetUpdated: The investigation limits a channel worker was shown, recorded when they changed. Deposited by the budget signal mid-run, or at run start when the limits differ from the ones last shown — the history still states those. Non-waking, like ``InstructionChanged``. Rendered only in the drain it lands in and never in compressed history, because each run's prefix states that run's limits.
            - `kind` 'usage_budget_updated'
            - `updated_at` string, date-time, required
            - `limits` InvestigationLimitsSnapshot, required: The finite investigation limits that are one worker's own, as of ``as_of``. Only budgets counted against the worker itself — or, for a private chat, its owner — appear. Budgets shared across the organization are left out, so an empty ``limits`` states that nothing caps this worker's own starts, not that nothing could refuse one.
              - …
            - `change` 'admin_changed' | 'refilled': Why a worker's investigation limits differ from the ones it was last shown.
          - ModeChanged: An operator changed this worker's standing mode. Records the transition rather than just the destination: "custom" alone leaves the worker unable to tell whether this is the mission it has always had, while "incident to custom" explains a change in its own behaviour it would otherwise have no account of. Both ends come from the edit that made the change, because the worker being started to record one has already resolved the new mode from the row and so cannot observe what it replaced. The row remains the authority on what the worker becomes; this is a description of the transition, not the mechanism for it. ``sender`` is the same actor shape a message event carries, resolved at edit time, so the feed renders who made the change from the event alone rather than looking an id up. None when the change had no attributable operator.
            - `kind` 'mode_changed'
            - `changed_at` string, date-time, required
            - `from_mode` 'incident' | 'alerts' | 'custom', required: The kind of channel the agent lives in — its standing mission. Drives the swappable mode block in the system prompt (and, later, the toolset). ``escalation`` and others land as localized drop-ins (their standing block + tooling) when that work begins. ``incident`` and ``alerts`` each state a mission the worker owns whatever the operator writes. ``custom`` states none: it supplies only the conduct floor the identity block forward-references, and the operator's ``custom_instruction`` is what gives the worker something to do. A channel that is neither working an incident nor triaging alert fires gets this one rather than being told it is in an incident (PRD-3766). The value is what customer-facing surfaces render, so it reads as the operator's own word for it rather than as an internal category. A member's *name* is persisted, in ``resident_agent_workflow.mode`` and ``resident_agent_template.mode`` — plain varchars with no DB-side value set — and read back through this enum, so adding a member is one deploy but removing or renaming one orphans every row holding it. It also makes a member forward-only: code that predates it raises on a row that carries it, rather than degrading. So a new member wants its read deployed everywhere before anything can write it — either landing the member ahead of the paths that create it, or gating creation while it rolls out. The same names are stored in usage policy bodies. Renaming or removing a member stops a body naming it from parsing, and that disables the template's policies.
            - `to_mode` 'incident' | 'alerts' | 'custom', required: The kind of channel the agent lives in — its standing mission. Drives the swappable mode block in the system prompt (and, later, the toolset). ``escalation`` and others land as localized drop-ins (their standing block + tooling) when that work begins. ``incident`` and ``alerts`` each state a mission the worker owns whatever the operator writes. ``custom`` states none: it supplies only the conduct floor the identity block forward-references, and the operator's ``custom_instruction`` is what gives the worker something to do. A channel that is neither working an incident nor triaging alert fires gets this one rather than being told it is in an incident (PRD-3766). The value is what customer-facing surfaces render, so it reads as the operator's own word for it rather than as an internal category. A member's *name* is persisted, in ``resident_agent_workflow.mode`` and ``resident_agent_template.mode`` — plain varchars with no DB-side value set — and read back through this enum, so adding a member is one deploy but removing or renaming one orphans every row holding it. It also makes a member forward-only: code that predates it raises on a row that carries it, rather than degrading. So a new member wants its read deployed everywhere before anything can write it — either landing the member ahead of the paths that create it, or gating creation while it rolls out. The same names are stored in usage policy bodies. Renaming or removing a member stops a body naming it from parsing, and that disables the template's policies.
            - `sender` ChatDeltaActor: The normalized identity of a message sender or mutation actor.
              - …
          - ScheduledWake: A scheduled directive accepted by the Worker for a normal turn.
            - `kind` 'scheduled_wake'
            - `occurred_at` string, date-time, required
            - `occurrence_id` string, uuid, required
            - `schedule_id` string, uuid, required
            - `schedule_type` 'one_shot' | 'recurring', nullable
            - `directive` string, required
            - `title` string
            - `created_by` string, nullable
            - `created_at` string, date-time, nullable
            - `source_thread_id` string, nullable
          - AssistantTurn: The LLM's response for one turn, plus the drain boundary that fed it. ``drained_through`` is the workflow-clock timestamp at which the drain that fed this turn happened — before the LLM call. The renderer uses it to batch events: an event is drained into this turn iff its ``observed_at`` / ``fired_at`` / ``occurred_at`` is ``> prev_turn.drained_through`` and ``<= this_turn.drained_through``. Tool calls inside ``message.tool_calls`` pair with matching ``SlackSend`` / ``SlackReact`` / ``InvestigationCall`` (stub) / ``WaitEntry`` entries by ``tool_call_id``; the renderer emits each tool response right after this assistant message in the same order as ``tool_calls``. ``new_transcriptions`` holds file transcriptions produced during this turn's drain (the workflow runs upload + transcription as part of the drain step). The renderer passes them to ``format_deltas`` so the ``<file_transcriptions>`` XML block lands co-located with the chat lines that reference the files — mirroring the existing per-batch emission.
            - `kind` 'assistant_turn'
            - `drained_through` string, date-time, required
            - `occurred_at` string, date-time, required
            - `message` WorkflowMessage, required
              - …
            - `new_transcriptions` FileTranscriptionResult[]
              - …
            - `llm_call_id` string, nullable
      - ChatbotMessageEventItem
        - `source` 'chatbot_messages'
        - `id` string, required
        - `worker_id` string, required
        - `occurred_at` string, date-time, required
        - `event_type` string, required
        - `data` union, required
          - SlackChatMessageSnapshot
            - `surface` ChatSurfaceSnapshot, required: The surface and channel where a message was observed.
              - …
            - `message_id` string, required
            - `thread_id` string, nullable, required
            - `sent_at` string, date-time, required
            - `edited_at` string, date-time, nullable
            - `sender` ChatDeltaActor, required: The normalized identity of a message sender or mutation actor.
              - …
            - `message` string, nullable
            - `files` FilePart[]
              - …
            - `reactions` ReactionEntity[]
              - …
            - `subtype` string, nullable
            - `permalink` string, nullable
            - `deleted_at` string, date-time, nullable
            - `platform` 'slack'
            - `rich_content` SlackRichContentEntity: Slack Block Kit content stored with a normalized message.
              - …
            - `user_id_to_display_name` object
            - `user_id_to_profile_photo_url` object
            - `channel_id_to_display_name` object
          - TeamsChatMessageSnapshot
            - `surface` ChatSurfaceSnapshot, required: The surface and channel where a message was observed.
              - …
            - `message_id` string, required
            - `thread_id` string, nullable, required
            - `sent_at` string, date-time, required
            - `edited_at` string, date-time, nullable
            - `sender` ChatDeltaActor, required: The normalized identity of a message sender or mutation actor.
              - …
            - `message` string, nullable
            - `files` FilePart[]
              - …
            - `reactions` ReactionEntity[]
              - …
            - `subtype` string, nullable
            - `permalink` string, nullable
            - `deleted_at` string, date-time, nullable
            - `platform` 'teams'
            - `rich_content` object, nullable
            - `user_id_to_display_name` object
            - `channel_id_to_display_name` object
          - WebChatMessageSnapshot
            - `surface` ChatSurfaceSnapshot, required: The surface and channel where a message was observed.
              - …
            - `message_id` string, required
            - `thread_id` string, nullable, required
            - `sent_at` string, date-time, required
            - `edited_at` string, date-time, nullable
            - `sender` ChatDeltaActor, required: The normalized identity of a message sender or mutation actor.
              - …
            - `message` string, nullable
            - `files` FilePart[]
              - …
            - `reactions` ReactionEntity[]
              - …
            - `subtype` string, nullable
            - `permalink` string, nullable
            - `deleted_at` string, date-time, nullable
            - `platform` 'web'
        - `presentation` 'feed' | 'thread_origin'
        - `reply_count` integer, nullable
      - TranscriptSummaryEventItem
        - `source` 'transcript_summary_events'
        - `id` string, required
        - `worker_id` string, required
        - `occurred_at` string, date-time, required
        - `event_type` string, required
        - `data` TranscriptEvent, required: Canonical state for one observed call-transcript window. The off-channel counterpart to `SlackEvent`: it lives in `ConversationState.transcript_log`, not `slack_log`, because a call window isn't in Slack and can't be rebuilt from the Slack API on catch-up — so the full log rides the workflow input across `continue_as_new`. `observed_at` (workflow clock) is the merge key shared with `SlackEvent`, so the drain renderer can order a transcript window against Slack messages.
          - `kind` 'transcript'
          - `observed_at` string, date-time, required
          - `source_label` string, required
          - `window_start` string, date-time, required
          - `window_end` string, date-time, required
          - `points` AttributedPoint[]
            - `speaker` string, required
            - `point` string, required
          - `idempotency_key` string, uuid, required
  - `next_cursor` WorkerEventCursor: Composite keyset cursor for the newest-first feed. ``occurred_at`` is the cross-source recency key; ``source`` + ``id`` are the deterministic tie-breaks. The client echoes these back to fetch the next page.
    - `occurred_at` string, date-time, required
    - `source` 'resident_agent_event' | 'transcript_summary_events' | 'chatbot_messages', required: Feed-layer discriminator for which table a feed row came from. This is a feed concept, distinct from the cursor-stream ``source`` protocol: the feed renders persisted rows (including non-streamable ``chatbot_messages``), it does not tail cursor streams. These values are the canonical vocabulary bound into ``worker_feed_dao._FEED_QUERY``.
    - `id` string, required
  - `prev_cursor` WorkerEventCursor: Composite keyset cursor for the newest-first feed. ``occurred_at`` is the cross-source recency key; ``source`` + ``id`` are the deterministic tie-breaks. The client echoes these back to fetch the next page.
    - `occurred_at` string, date-time, required
    - `source` 'resident_agent_event' | 'transcript_summary_events' | 'chatbot_messages', required: Feed-layer discriminator for which table a feed row came from. This is a feed concept, distinct from the cursor-stream ``source`` protocol: the feed renders persisted rows (including non-streamable ``chatbot_messages``), it does not tail cursor streams. These values are the canonical vocabulary bound into ``worker_feed_dao._FEED_QUERY``.
    - `id` string, required
  - `has_more` boolean, required
  - `has_more_newer` boolean
  - `anchor_found` boolean, nullable
  - `stream_cursors` object, nullable

## Other responses

- `422`: Validation Error

## Changes

> 66 revisions in range; 9 not diffed.

- **2026-10-06** `31e1a8b7cf89`: 1 breaking, 3 info
  - added `#/components/schemas/UsageBudgetUpdated` to the `events/items/oneOf[subschema #1: ResidentAgentEventItem]/data` response property `oneOf` list for the response status `200`
  - added the optional property `events/items/oneOf[subschema #1: ResidentAgentEventItem]/data/oneOf[subschema #2: InvestigationCall]/usage_limit_refused` to the response with the `200` status
  - added the optional property `events/items/oneOf[subschema #1: ResidentAgentEventItem]/data/oneOf[subschema #2: InvestigationCall]/usage_limits` to the response with the `200` status
  - added `usage_budget_updated` discriminator mapping keys to the `events/items/oneOf[subschema #1: ResidentAgentEventItem]/data` response property for the response status `200`
- …earlier changes not shown

[Operation history](https://skmtc.dev/traversal/apis/traversal-api/changes/worker/:worker_id/events/get.md)

---

[API](https://skmtc.dev/traversal/apis/traversal-api.md) - [All operations](https://skmtc.dev/traversal/apis/traversal-api/llms.txt) - [OpenAPI document](https://skmtc.dev/traversal/apis/traversal-api/revisions/31e1a8b7cf89?raw)
