A3 — Visibility, Search Attributes, and Operating from History (long form of Module 10)

On this page

The capstone requirement that gets forgotten until the first incident is "operators need exact execution diagnostics." Event History gives you exact for one run. Visibility gives you find across thousands. This page is about the second, and about which tool answers which question when a run has gone wrong.

Swipe to explore diagram →
Find across runs, then inspect one execution A List Filter searches the eventually consistent Visibility index and returns matching Workflow IDs. After selecting agent-42, inspect its ordered Event History, current pending work with Describe, or live application state with a Query that needs a Worker. Find across runsInspect one run List Filterpaused runs Visibilityfleet index matching IDsagent-42, agent-87 choose one IDagent-42 History: Events Describe: pending Query: live state Visibility can lag; a Query needs a Worker.
Use Visibility to find matching execution IDs. Once you have an ID, History and Describe inspect that execution; a Query reads live Workflow state when a Worker is available.

What Visibility is

The term Visibility, within the Temporal Platform, refers to the subsystems and APIs that enable an operator to view, filter, and search for Workflow Executions that currently exist within a Temporal Service. Visibility enables the listing, filtering, and sorting of Workflow Executions through a custom SQL-like List Filter, and supports custom Search Attributes for user-defined filtering beyond the default system attributes.

Two properties shape how you use it. First, Visibility is a search index that Temporal updates asynchronously: after a change is recorded it takes some time to propagate, so a List or Count query can briefly return stale results. Treat it as eventually consistent. Second, and following from that, Visibility is built for finding and filtering across many executions, not for reading the current state of one execution. When you need the authoritative, up-to-date state of a specific Workflow Execution, use DescribeWorkflowExecution instead of a Visibility query. Operations that look up a single entity by ID, such as DescribeWorkflowExecution, do not use Visibility.

Upstream is also clear about what not to do with it: to check whether a specific Workflow exists, start it and handle the already-started result, or look it up by ID; to react to a Workflow's progress, follow its Event History or use Child Workflows; to coordinate between Workflows, use Signals, Updates, or Child Workflows rather than reading Visibility from inside Workflow or Activity code. Visibility is for the operator's dashboard, not for AgentRun's control flow.

Search Attributes: default and custom

A Search Attribute is an indexed field used in a List Filter to filter a list of Workflow Executions that have the Search Attribute in their metadata. Each Search Attribute is a key-value pair metadata object included in a Workflow Execution's Visibility information.

Default Search Attributes are created when the initial index is created, are set globally in any Namespace, and are reserved and read-only. The ones you will query for AgentRun (subset; full table upstream, as of 2026-09):

Name Type Meaning
WorkflowId, RunId, WorkflowType Keyword identity of the execution
ExecutionStatus Keyword Running, Completed, Failed, Canceled, Terminated, ContinuedAsNew, TimedOut
StartTime, CloseTime, ExecutionTime Datetime ExecutionTime differs from StartTime for retried and Cron Workflows
TaskQueue Keyword the Workflow's Task Queue (agent-workflows in A1's topology)
HistoryLength, HistorySizeBytes, StateTransitionCount Int / Long history growth; HistoryLength and StateTransitionCount are present only for closed executions
TemporalWorkerDeploymentVersion, TemporalWorkflowVersioningBehavior Keyword which Worker Deployment Version the run is on and whether it is Pinned or Auto-Upgrade (module 9)
TemporalReportedProblems Keyword List Workflow Task failures, as category=<category> cause=<cause>

BinaryChecksums and BuildIds are deprecated in favour of the TemporalWorkerDeployment* attributes (removal scheduled for server 1.32–1.34, as of 2026-09; see upstream page).

Custom Search Attributes are keys you create with a name and one of seven types: Bool, Datetime, Double, Int, Keyword, KeywordList, Text. Most custom Search Attributes store structured identifiers, so Keyword is the right default choice: it stores the value as-is and supports =, !=, IN, STARTS_WITH, BETWEEN, and ORDER BY. Use KeywordList for tags, and Text only for prose you want word-level search on — Text tokenises on word boundaries and the = operator does OR matching across tokens, which surprises people who put IDs in it.

Three rules, all upstream:

Limits worth knowing (as of 2026-09; see upstream page): per Namespace on the SQL Visibility stores, 10 Keyword, 3 of each other type, 40 Keyword and 20 of most others on Temporal Cloud; a single value up to 2 KB and 255 characters; 40 KB total per execution. The dev server in the labs uses SQLite, so the SQL limits apply.

How AgentRun exposes AgentStatus, CurrentStep, Owner

Create the keys once per Namespace:

temporal operator search-attribute create --name AgentStatus --type Keyword
temporal operator search-attribute create --name CurrentStep --type Int
temporal operator search-attribute create --name Owner --type Keyword

Declare the same keys in code (names verified against temporalio 1.32.0, as of 2026-09; see upstream page):

# shared.py
from temporalio.common import SearchAttributeKey

AGENT_STATUS = SearchAttributeKey.for_keyword("AgentStatus")
CURRENT_STEP = SearchAttributeKey.for_int("CurrentStep")
OWNER = SearchAttributeKey.for_keyword("Owner")

Set the ones known at start from the Client:

from temporalio.common import SearchAttributePair, TypedSearchAttributes

handle = await client.start_workflow(
    AgentRun.run,
    task,
    id=f"agent-{task.id}",
    task_queue=AGENT_WORKFLOWS,
    search_attributes=TypedSearchAttributes([
        SearchAttributePair(OWNER, task.owner_id),
        SearchAttributePair(AGENT_STATUS, "planning"),
        SearchAttributePair(CURRENT_STEP, 0),
    ]),
)

Upsert the ones that change, from inside the Workflow, at the points where the loop's state changes:

@workflow.defn
class AgentRun:
    def _publish(self, status: str) -> None:
        self.status = status                          # the Query reads this — authoritative
        workflow.upsert_search_attributes([           # the fleet view reads this — eventual
            AGENT_STATUS.value_set(status),
            CURRENT_STEP.value_set(self.step),
        ])

    @workflow.run
    async def run(self, task: Task) -> Result:
        while not self.done:
            self._publish("planning")
            step = self.plan()
            self._publish("calling_llm" if step.kind == "llm" else "calling_tool")
            out = await workflow.execute_activity(...)
            await workflow.wait_condition(lambda: not self.paused)   # Signals pause/resume
            self.step += 1
            if self.step % SNAPSHOT_EVERY == 0:
                workflow.continue_as_new(self.snapshot())            # keys carry over
        self._publish("done")

Set self.paused = True in the pause Signal handler and call self._publish("paused") there too, so a paused run is findable. upsert_search_attributes is a Command like any other: it is recorded in History (UpsertWorkflowSearchAttributes), replays deterministically, and issues no new Command on replay. The Query status remains the strongly consistent answer for one run; the attributes exist so an operator can ask about all runs.

List Filter

The Visibility List API requires you to provide a List Filter as an SQL-like string parameter, made of Search Attribute names, values, and operators. Names are case sensitive, and a single Namespace scopes each List Filter. Supported operators: =, !=, >, >=, <, <=, AND, OR, (), BETWEEN ... AND, IN, STARTS_WITH (Keyword only), IS NULL / IS NOT NULL. ORDER BY is not supported on Temporal Cloud and never on Text attributes (as of 2026-09; see upstream page). The same string works in the Web UI's filter box, temporal workflow list --query, and client.list_workflows().

Queries an operator of the agent runtime actually types:

-- everything paused, across all owners
WorkflowType = 'AgentRun' AND AgentStatus = 'paused'

-- one customer's live runs
Owner = 'acct_7f3a' AND ExecutionStatus = 'Running'

-- runs that have been grinding: past step 500 and still going
WorkflowType = 'AgentRun' AND CurrentStep > 500 AND ExecutionStatus = 'Running'

-- runs that ended badly in the last deploy window
WorkflowType = 'AgentRun' AND ExecutionStatus IN ('Failed', 'TimedOut', 'Terminated')
  AND CloseTime > '2026-09-10T08:00:00Z'

-- which Worker version are the pinned runs on?
WorkflowType = 'AgentRun' AND TemporalWorkflowVersioningBehavior = 'Pinned'
  AND TemporalWorkerDeploymentVersion = 'agent-workflows:v42'

-- the whole family of one job, parent and ResearchAgent children
WorkflowId STARTS_WITH 'agent-task-123'

-- closed runs whose history got long (HistoryLength exists only once closed)
WorkflowType = 'AgentRun' AND ExecutionStatus != 'Running' AND HistoryLength > 1000
temporal workflow list --query "WorkflowType = 'AgentRun' AND AgentStatus = 'paused'"
temporal workflow count --query "WorkflowType = 'AgentRun' GROUP BY ExecutionStatus"

The Count API returns approximate counts, and GROUP BY is supported only there and only on ExecutionStatus (as of 2026-09; see upstream page). From Python, client.list_workflows(query) is an async iterator and client.count_workflows(query) the count. Two idioms to remember: WorkflowId STARTS_WITH 'agent-' for a prefix, and WorkflowId BETWEEN 'agent-' AND 'agent-~' when you need a bounded range. If you ever name a custom attribute the same as a default one's un-prefixed alias (SchedulePaused vs TemporalSchedulePaused), the custom one wins and the Temporal-prefixed name still reaches the default (aliasing needs server 1.30+, as of 2026-09).

The operator's five questions

This section is ours. Module 10's third key example hands you a broken AgentRun history and asks you to diagnose it from the UI alone. The five questions below are the ones to answer, in order, and each has one surface that answers it.

Question What answers it CLI UI
What happened? the Event History, in order: which Activities were scheduled, started, completed, failed, timed out; which Signals arrived; which timers fired temporal workflow show -w <id> (--detailed for full attributes, --follow to tail a live run, --output json to feed a replay test) Workflow details → Event History
What did Temporal think happened? the execution's current view: status, the pending Activities with attempt count and last failure, pending Child Workflows, pending Nexus Operations, Search Attributes; plus the Workflow Task failures the Service recorded temporal workflow describe -w <id>; temporal workflow stack -w <id> for where the code is blocked Workflow details → summary, Pending Activities, Search Attributes; TemporalReportedProblems in the list
What side effects may have occurred? History and pending attempt count show what Temporal observed; heartbeat details show reported progress, not committed external effects. The provider or operation ledger is the authority, and an in-progress key may still have an ambiguous outcome after a crash temporal workflow show and describe, then inspect the external ledger Event History; Pending Activities → Heartbeat Details; external ledger
What will happen next? the pending items in describe (which Activity is on which attempt, what its retry policy's next backoff is), an open TimerStarted in the history, and the Workflow code the stack trace points at temporal workflow describe; temporal workflow stack Pending Activities / Timers on the details page
What if every Worker disappears now? nothing in the execution changes. Workflow and Activity Tasks persist in the Task Queue until a Worker returns; the run resumes by replay. The only clock still running is the timeouts temporal task-queue describe --task-queue <lane> --task-queue-type activity — pollers gone, ApproximateBacklogAge rising; temporal workflow list --query "ExecutionStatus = 'Running'" for what is exposed Task Queues page; Workflow details → Workers tab (server 1.30+, as of 2026-09)

Notice which questions Visibility answers: only the fifth, and only the "how many, which ones" half of it. The first four are answered by History and Describe, per execution, strongly consistent. That split is the boundary the capstone keeps asking you to draw: Temporal's History tells you what execution happened; whether the result was good, and what the LLM provider actually charged, live in systems of record outside it.

Where this is used

Lab 11.2 (labs/11-capstone-durable-agent-runtime/) runs AgentRun for 200 steps with Continue-As-New and has you verify two things with the tools above: that no closed run shows HistoryLength > 700 (temporal workflow list --query "WorkflowType = 'AgentRun' AND ExecutionStatus != 'Running' AND HistoryLength > 700" returns nothing — the lab's measured runs are 516–519 events), and that AgentStatus, CurrentStep, and Owner survive each Continue-As-New and stay queryable while the run is live. Lab 10.2 is the five-question drill on a run you did not start (lab 8.5 is the forensic version, on a broken history with no source); write the five answers in the order of the table, naming the command or UI surface you used for each, and put that page in your Lab 11.4 defense.

Sources and license

This page adapts material from Temporal's MIT-licensed documentation and samples (© Temporal Technologies Inc.; © Uber Technologies, Inc.). Adapted text is rewritten for this course; the upstream pages are the reference of record and may have changed since the commit linked here. Temporal is a trademark of Temporal Technologies; this course is independent and not endorsed by Temporal.