feat: include agent metadata in workspace list responses (#27934)

Closes #27933. Related: #27897 (single-agent GET).

Agent metadata is only readable via a per-agent watch stream, so reading
it across N workspaces costs N+1 requests. This adds a batch read to the
list endpoint:

```text
GET /api/v2/workspaces?q=param:"pool=demo" include_agent_metadata:task_status
```

- New `include_agent_metadata` search key, repeatable and key-scoped. It
expands the response, it does not filter workspaces.
- `GetWorkspaces` aggregates the requested keys as JSON behind a `CASE`:
without opt-in the response is unchanged and the subquery never runs.
Runs only for the returned page, inside the same authorized query.
- Agents in the response gain `metadata`
(`[]codersdk.WorkspaceAgentMetadata`, `omitempty`), mapped by the
`workspace_agent_id` each element carries. The collection script is
omitted; it can be long.
- `codersdk.WorkspaceFilter` gains `IncludeAgentMetadata []string`.
- No wildcard, no schema change, no migration.

---

Authored by Coder Agents on behalf of @Emyrk.
This commit is contained in:
Steven Masley
2026-08-10 08:13:32 -05:00
committed by GitHub
parent a3a51228ee
commit 9a57dfa642
21 changed files with 1507 additions and 588 deletions
+5
View File
@@ -170,6 +170,11 @@ type WorkspaceAgent struct {
DisplayApps []DisplayApp `json:"display_apps"`
LogSources []WorkspaceAgentLogSource `json:"log_sources"`
Scripts []WorkspaceAgentScript `json:"scripts"`
// Metadata is only populated on the workspaces list endpoint when the
// request opts in with the include_agent_metadata search key, and it
// only carries the requested keys. The description's script is always
// empty here: it can be long, and list consumers want values.
Metadata []WorkspaceAgentMetadata `json:"metadata,omitempty"`
// StartupScriptBehavior is a legacy field that is deprecated in favor
// of the `coder_script` resource. It's only referenced by old clients.
+6
View File
@@ -560,6 +560,9 @@ type WorkspaceFilter struct {
SharedWithUser string `json:"shared_with_user,omitempty" typescript:"-"`
// SharedWithGroup is the group name, group ID, or <org name>/<group name> of the group that the workspace is shared with
SharedWithGroup string `json:"shared_with_group,omitempty" typescript:"-"`
// IncludeAgentMetadata expands each agent in the response with the
// named metadata keys. It does not filter the returned workspaces.
IncludeAgentMetadata []string `json:"include_agent_metadata,omitempty" typescript:"-"`
// FilterQuery supports a raw filter query string
FilterQuery string `json:"q,omitempty"`
}
@@ -595,6 +598,9 @@ func (f WorkspaceFilter) asRequestOption() RequestOption {
if f.SharedWithGroup != "" {
params = append(params, fmt.Sprintf("shared_with_group:%q", f.SharedWithGroup))
}
for _, key := range f.IncludeAgentMetadata {
params = append(params, fmt.Sprintf("include_agent_metadata:%q", key))
}
if f.FilterQuery != "" {
// If custom stuff is added, just add it on here.
params = append(params, f.FilterQuery)