Activity
Overview
Activity lives at/activity, under the Run group of the sidebar. It answers the question people arrive with — what is happening, and where do I look — as a stream: a left rail listing what has been active in the loaded window, and a main column that changes with whatever the reader picked.
The page is built from three independent reads:
- The workflow index —
GET /api/v1/chains(hooks/use-chains.ts), one row per chain. A chain is every run sharing onechain_origin: the rule or person that started it, the routine runs it caused, and the agent work those dispatched. It is fetched on demand, not streamed. - The routine runs —
GET /api/v1/workspaces/{ws}/pipeline-runs?since=and the run’s own records, which feed the home and the run page. - The journal —
GET /api/v1/journal, which feeds the filtered event list and the opened-event detail. It streams over SSE and falls back to polling.
/activity used to be a single-canvas run trace: a rail of runs, one run’s execution chain on a ReactFlow canvas, a step side panel. That page is gone — with its saved views, heatmap toolbar, step selection and inline waitpoint buttons. A run’s execution was not lost; see Where the run trace went.When to use it
- Something ran and you want to know how it went. A row opens the run — its status, why it failed or what it waits for, its steps with every attempt, what it caused further down the chain, what it is linked to and changed, and its routine’s previous runs.
- You open the page to see the day. The home says in one sentence how many runs there were, what is live, what needs you and what could not finish, and draws every run on its routine’s lane.
- You want a routine’s runs, not its definition. Group by → Routine opens a routine’s runs grouped by the hour, with each run’s own result on the row instead of its id. The definition stays on Routines.
- You want to know what your agents did. Group by → Agent lists agents that took work in the window, how much, and in which workflows.
- An issue moved and you want to know what moved it. Group by → Issue lists issues touched in the window with the routine and agent that reached each one.
- You are chasing a failure. The Could not finish status row narrows both the rail and the feed.
- You want accepted work or received webhooks. The Ledger rows at the bottom of the rail switch it to the work queue and webhook deliveries.
/journal (workspace OWNER and ADMIN only, see Crew Journal).
Key concepts
The rail
One column, and it is navigation, built from the same sidebar kit as Issues and Routines: a search box, a filter popover, a Group by button, then a collapsible Status section, the list, and the Ledger rows. Every narrowing lives in the popover; every place you can go is a row. There is no tab strip above the page and no second row of tabs in the rail.Group by
The View button (the rows icon beside Filter) groups the list. Each grouping lists only the members that were active in the loaded window. An issue nobody touched today is in Issues, not here.
Under Time, every chain of the window is listed and grouped into Active now, Today and Earlier. “Active now” is a state, not a time: a chain parked on a human approval since Tuesday is the most urgent row on the page and must not sort to the bottom by its timestamp.
Each row wears its routine’s own icon and colour (the same derivation the Routines rail uses), a status dot notched into the corner, a halo while something is live, the routine’s name,
could not finish / waiting for you · what started it · duration · what it touched, and how long ago it was active.
Status
A collapsible section with one row per bucket, in the Routines rail’s order and words: All, Waiting for you, Running, Completed, Stopped, Could not finish. Stopped is a chain whose runs were cancelled or interrupted and that holds nothing live: a chain with a stopped branch and a running or waiting one is counted as Running or Waiting, so a stop never hides live work. On a run’s page the word says which stop it was — Cancelled (somebody stopped it) or Interrupted (the process running it died). In the feed, Stopped narrows torun.cancelled and assignment.cancelled. Every row is always there — a bucket holding nothing dims rather than disappearing, so no row moves because its neighbour emptied. The count pill and the selected row are the Routines rail’s, to the class (railStatusRows in lib/activity-rail.ts).
The counts are taken over the window with the search applied but not the status, so a number survives its own selection: counting after the pick would render “Could not finish 3 · Waiting 0 · Running 0” — what is left rather than what there is to pick.
The same choice narrows the journal query server-side (active/waiting become an entry_type filter, failed becomes severity=error); done is the one bucket with no server-side expression, so it is filtered client-side.
Waiting is narrowed twice, and the second pass is the one that matters. The journal is an event log, so the entry_type query returns ask-shaped rows — including an approval.requested whose approval was granted an hour ago, and a peer.escalation that is the end of one (ask and answer share that one entry type, separated only by payload.state: pending is the ask, and resolved, expired and cancelled are the three ways it ends). A row is only still waiting on somebody while its ask is open, so each ask is joined to its answer on kind + id over the loaded window, and the answered ones fall into Completed.
The join needs both halves in the window, so the waiting query asks for the answers as well as the asks — approval.granted / denied / cancelled / timeout and keeper.decision, which are filed under the Security facet and would otherwise be excluded server-side. They never reach the feed: the same narrowing that retires their asks files them under Completed.
That costs window. The journal pages by a fixed row count from the newest end, so answer rows occupy slots that asks used to have, and an open ask pushed past the last row would drop out of Waiting altogether — the opposite of the bias below. Only this scope therefore pages to the API’s ceiling (500 rows rather than 300), which holds comfortably more asks than the ask-only query did. Beyond that ceiling the boundary is the usual one, with Load older under the list.
An ask whose answer lies outside the window, or that carries no id to join on, stays under Waiting: over-reporting one row is a smaller failure than hiding something a person is blocking on. An escalation in a state this classifier has never seen is treated the same way — unrecognised is not the same as answered.
One function (scopeOf in lib/activity-stream.ts) does this for the feed list, every row’s status dot and the Overview’s Waiting on you card, so those three cannot disagree. The rail’s status rows are the exception, and deliberately so: they count chains (chainScopeCounts over the ChainSummary index, ultimately the server’s status column) rather than journal rows, because the chain index is fetched independently of the scope facet and so can still answer for the buckets the journal query did not fetch. A status row counts chains; the feed beside it counts entries.
Focusing a routine
Picking a routine’s row narrows the rail to that routine (activity-rail-focus.tsx, rules in lib/activity-rail-focus.ts). Everything else in the rail goes. The routine’s runs are listed by day (Today, Yesterday, then dates), each with its time, its outcome in the rail’s words, what started it and how long it took.
The STATUS rows stay and count the routine’s runs, so Could not finish lists only the runs that failed. The time range from Filter decides how far back the list reaches.
Clicking a run opens it in the column, and the focus stays while you step through its runs. ‹ All activity leaves the focus. The rail slides in from the right on entering the focus and from the left on leaving it, the same 12px / 200ms entrance as the column.
What a row is
Every row’s second line starts with what the row is:- Routine: a routine run and what it caused;
- Issue: an agent working an issue;
- Agent: agent work delegated from a chat or assigned by hand.
Search and filters
The Filter facets narrow the rail as well as the column beside it.- A picked crew keeps the rows whose agents belong to it, or whose routine that crew authored.
- A picked agent keeps the rows it worked in.
- A focused issue or routine keeps the rows that reached it.
q).
The Filter trigger carries a count of active narrowings and opens a popover holding:
error is deliberately absent from Severity and human from Source: they are the Could-not-finish and Waiting-for-you status rows, and one filter reachable from two controls is how the two quietly contradict each other. See lib/activity-rail.ts.emptyLensCopy), because they want different actions from the reader.
The main column
One selection, one column — never a stack.The home
The time view’s home is built from runs, not journal events, and answers in the order a person asks on arrival (components/features/activity-stream/activity-home.tsx, rules in lib/activity-home.ts):
A crew, an agent, an issue or routine focus, or a search narrows the home to the same chains the rail shows, and the heading names the narrowing (Today · Operations). Its runs are the runs of those chains, matched by
chain_origin. Its asks are the asks of those runs, and Up next lists only their routines’ schedules.
Narrowings only the journal can express get the filtered event list instead: a source, a severity, the telemetry switch, a pinned crumb. The rail then lists only the runs those events belong to, matched by the run id an event carries and the issue it names. When no event is left, no run is left either.
Issues, Agents, Routines
Each grouping owns the column, so picking one changes what is shown and not only what is selected.- Issues — Touched / Created by agents / Changed / Agents involved, then one line per issue carrying the issue’s real status and priority glyph, with an indented
↳ routine → agentline that opens the workflow behind it. - Agents — Agents at work (of N hired) / Assignments / In workflows / Busiest, then one line per agent with a proportion bar and what it worked on. “In workflows” is the wall clock of the chains the agent worked in, not billed agent time — the index does not carry that.
- Routines — Routines that ran (of N in the catalogue) / Runs / Success / Failing, one line per routine with a success-rate bar, plus a Firing next card listing the schedules due next with their cron and a countdown.
A run page
A rail row whose chain starts at a routine run opens that run (activity-run-page.tsx), and so does every other way to a run on this page — a sub-run, a dot in Previous runs, a bar on the home, ?run= from Inbox or the bell. It is built from the parts Routines uses, so a run reads the same on both pages:
A run id that is not a routine run — agent work — falls back to the typed run detail.
Agent work outside routines
Hidden in the UI for now, at the owner’s call (#3007). The chain index still returns these rows (
kind: assignment), and the CLI lists them.from chat · Lead, from OPS-3, lead planning · OPS-3, by hand.
The row opens the work’s page (activity-work-page.tsx), shaped like a run’s page:
- the task;
- the state in the rail’s words;
- what started it;
- Started / Duration / Pieces of work / Could not finish / Agents;
- Delegations and asks: the assignments it delegated and the
run_needs_humanasks it raised, nested; - Linked to: the issue it was for and the agents who did it.
docs/specs/activity-work-index.md).
A workflow page
A chain rooted in agent work has no run to open, so its row opens the chain as a page, answering five questions in order:- What was this? — the sentence as the heading, who started it, the raw
triggered_via, when (absolute and relative), and the chain origin. Then a strip: Duration, Runs, Failed, Depth, Issues, Agents. - Which runs? — the Runs card, derived from the chain walk and filtered to this chain’s origin. Each row opens that run.
- How did it happen? — the topology card: the causal graph from
GET /api/v1/chains/{anchor}, drawn left to right. Nodes are clickable and walk one level down. - What happened, then? — What happened, in sequence, beside the graph: the same nodes as dated rows, indented by cause, time in a fixed left gutter and duration in a fixed right one.
- What did it touch? — the issues and agents, clickable.
Where a row leads
Each kind gets the surface that kind already has elsewhere, not a fifth rendering of the feed.A routine’s runs
Thirty runs of a one-minute routine are thirty rows that read the same, so this page makes three deliberate choices (all pure functions inlib/run-digest.ts):
- Absolute time, to the second — the reader already knows it was recent and is asking which one.
- The run’s own result instead of its id — the first line of its output (“3 tickets classified”), or the error for a failure, or a
slow — 4.2snote when the run is past 1.4× its peers’ median. An empty output renders as a dash rather than a stock phrase repeated on every row. - An hour header that summarises — “12 runs · all ok” is skippable; “3 runs · 1 failed” is where you stop. The header carries its date only when the page holds more than one day.
This page’s “failed” folds in
cancelled and interrupted, while a chain row’s failed_runs does not. That is not a bug to align: a chain row answers did this workflow fail, where a process killed by a deploy is the operator’s problem; this page answers which of these thirty runs did not produce the thing I came for, and a run killed mid-flight produced it exactly as little as one that threw.Opening one event
A row in the feed opens as a full-width page, not a side panel — an activity has a shape, and a 340px column is where that shape goes to die. It carries: four KPIs (this event, chain duration, chain cost, errors in chain), an Execution graph for the run behind the event, a topology card, the Chain (every journal entry under the same trace, oldest first), and the raw Record.The execution graph here is the same
TraceCanvas component the old trace page drew — but inline waitpoint decisions and heatmap shading are not wired to it. Decide waitpoints in Inbox.Where the run trace went
This is the question every reader of the old page arrives with. The trace canvas as a page — pick a run from a rail, read its steps in a side panel — no longer exists. What it showed is in three places:
There is no step object on this page: a run’s steps are its drill-down. Saved views, the heatmap toolbar, group-by axes and keyboard step selection went with the canvas and have no replacement.
The URL is the state
Whatever you are looking at on/activity is in the address bar, and a change of position is a history entry: Back closes the drill-down you just opened, a reload resumes the walk, and the page can be sent to someone.
Position (the walk, the opened record) is pushed; grouping and status are replaced, so Back does not step through every choice you tried. Ids read from a URL show as ids until the entities they name load, then the trail re-labels them.
A bare
?run= with no routine used to render “This run’s record is not loaded”, because the record lives in the routine’s run list and the link carried no routine. The run page now asks the run API for the record and resolves the routine (or agent) behind it from there. Under the run’s heading a Related row links its routine, its issue, its agent, its crew and its own journal trace — the second leg of the one timeline (issue → run → journal → issue).
Deep links
Fourteen places across the product link into/activity with a query string the trace canvas defined. Those links still work — the parameters are translated rather than dropped (lib/activity-deeplink.ts) and written back in the same spelling.
Two rules are worth knowing:
- Order is by specificity, not by the URL.
?mission=then?pipeline=then?run=are pushed as stops in that order, so?pipeline=<slug>&run=<id>— the link routine cards build — lands on the run with the routine behind it, and Back returns to the routine rather than to the overview. - A status this page has no row for is ignored rather than obeyed. A link written against some other vocabulary lands on everything, because a page that renders empty because it did not recognise a word is indistinguishable from a page with no data.
What the numbers can and cannot say
Every count on this page is a claim about a window, and the page says so where the numbers are. Repeated here so nothing is implied by omission:- One page of the index. The Group by totals, the status rows and every KPI on every grouping’s dashboard are derived from the loaded chain index page — the newest 25 rows in the dashboard. When the workspace holds more, the rail prints “Newest N workflows. Every count on this page describes these, not the whole workspace.” (
has_morefrom the API; the client treats a missing field as “there are more”). The API itself defaults to 50 rows and caps at 200. - Group by → Agent sees only routine-dispatched work. The index groups
pipeline_runs, so an agent whose work no routine dispatched has no chain to belong to and is not indexed. That is a hole in the index, not in the grouping — and the empty state says it rather than reading as “nobody worked”. - Group by → Issue is issues touched in the window, not the backlog. An issue with no activity has no row here at all.
- Issue writes an agent makes from inside its own assignment do not attach to the chain. Those journal entries carry the assignment’s own journal run id as their trace, and that id is not recorded on the assignments row, so nothing joins them back. The issues a chain’s routine runs touched are complete; the ones its agents touched are not reachable. Stated as a KNOWN GAP on
chainIssuesQueryininternal/api/chains_list.go. - Chains whose runs predate
chain_originare not in the index at all. The link was never written and cannot be reconstructed, so listing them would mean claiming each run was its own chain root. Their existence is reported instead — the rail prints “Older runs are not indexed here — the link that would group them was never written.” - A row’s issue and agent lists are capped at 5 each, with the exact totals alongside, so a cut list reads as cut (“+N more, not carried on this row”).
- A routine, run or step crumb narrows the loaded window only — those ids live inside the journal payload, which the API cannot filter on. The chip says
(loaded window)rather than implying a full-table match. - A workflow chip narrows nothing. The journal has no
chain_origincolumn, so a chain is not expressible as a filter over journal rows; picking one re-points the column. It is therefore not counted by the “none satisfies all N filters” banner. - The index is fetched, not streamed. A row can be picked and then swept from the list — the page says “This workflow is no longer in the index” rather than drawing a graph-shaped hole.
Every run is listed
The Time grouping used to list only chains that composed something and sent a plain run of a known routine to the Routines lens, with a sentence in its place. On a quiet workspace that left the rail empty while a run had happened. The rail is where a run is found, so every chain of the window is a row now; whether it composed anything is what its sentence and its page show.Keyboard
On a run’s page, ↑ and ↓ step to the previous and next run in the rail’s order (Active now, Today, Earlier). The ends stay put, and the keys do nothing while you type in a field.j/k— step to the next or previous event in the feed, opening it. From nothing open,jopens the newest.Esc— close an opened event; with nothing open it steps one stop back up the trail, rather than doing nothing once you are already at the top.
Realtime
The journal feed subscribes to the workspace stream and prepends live entries; the badge next to the event count reads live (SSE), polling (the stream is unavailable, falling back to a 5s poll) or connecting. There is no Pause and no Refresh: the surface is always live, so a Refresh button would only advertise a staleness that is not there and a Pause is a way to be shown old numbers by accident. The chain index is not streamed. The rail is a place you return to between actions, not a tape you watch — one query per look, rather than one per event, and no re-sorting under the cursor.API and CLI
Activity is a shell over existing endpoints; each has a CLI command with the same shape.
Schemas:
/api-reference/chains, /api-reference/journal, /api-reference/runs. CLI pages: crewship chain, crewship activity.
The legacy /orchestration route still redirects to /activity so old bookmarks keep working — and so does the retired Work page: /work (with its ?section=work|deliveries view) opens the corresponding ledger.
Ledgers
Two places sit under Ledger at the bottom of the rail: the Work queue (?section=work), every piece of accepted work — a webhook, a chat turn, a schedule, an assignment — with its attempts and result; and Webhook deliveries (?section=deliveries), every received webhook, including the ones the filter ignored.
Opening one keeps Activity’s shape. The rail slides over to the ledger’s own rows — ‹ All activity leads back, and slides the run list in again from the left:
- a Work queue · Deliveries switch;
- Status in Activity’s words and colours: Needs you, In the queue, Running, Done, Failed, Cancelled for work; Accepted and Ignored for deliveries;
- Agents with their own avatars, and whether their queue moves — idle, running, blocked, deleted; on deliveries, Endpoints with when the last delivery came, or quiet, blocked, deleted;
- Events, grouped by family (
invoice.*,ping).
- one sentence — how many pieces of work, how many need you, are in the queue, finished, failed;
- Flow — Arrived → In the queue → Running → Done, with Needs you, Failed and Cancelled beside it; a step narrows to it;
- Needs you — work whose outcome nobody knows (
needs_reconciliation). It holds the agent’s only slot, so everything sent to that agent waits behind it, and the card says how much. A MANAGER or above settles it here: Retry (mark failed, then run the same input again under your name), Mark as failed or Mark as done. Each asks what you checked and that nothing is still running. Retry is two steps: when the input cannot be run again (the webhook payload passed its retention, or the work belongs to a private workflow) its dialog says why and does not run it; if running it again is refused after the work was marked failed, the work stays failed and a message says so; - Why work failed — failures grouped by cause, with the agents and events affected;
- What came in — one lane per agent, with its crew in its own colour and icon; every piece of work is a dot that opens it. Past eight agents the rest fold into ”+ N quieter agents”;
- Latest work — one sentence per piece: the event, the agent who took it, and how it ended.
Activity Bar (toolbar)
The/activity page answers “what happened” in depth; the Activity Bar answers “what is running right now” at a glance, from anywhere in the app. It is the Activity icon in the top toolbar next to the Inbox bell, with a badge counting live runs — amber the moment anything waits on a human approval, blue while routines run.
The dropdown speaks the rail’s words. It holds two sections:
- LIVE: up to six active routine runs, plus any in-flight agent runs. Each routine run shows its current step, elapsed time and cost.
- A run that waits on a person says waiting for you, and Decide → opens the run’s page, where the approval is.
- Open run opens the run’s page.
- Cancel is offered only to OWNER and ADMIN, the roles the cancel API accepts.
- RECENT: the last few finished runs, so “did my run just finish?” costs no page hop. A run reads Completed, Could not finish, Cancelled or Interrupted, with the rail’s colours; a stopped run is grey, not green.
/activity?status=waiting when something waits on a person, /activity?status=active while anything is live, and /activity otherwise.
The server is the source of truth: GET /api/v1/runs?status=RUNNING for agent runs and GET /api/v1/workspaces/{ws}/pipelines/runs/active for routine runs, re-fetched on a short poll and immediately on assignment.updated, run.*, mission.updated, pipeline.run.* and pipeline.step.started — so a dropped frame cannot leave the badge stale. Nothing in flight, and the badge disappears.
Run activity rail
Individual issue and routine pages carry an inline Run activity rail — the readable, human-first timeline of what an agent actually did during a run, built from the journal. Each step is one line: a toned icon, the time, a plain-language title (“Ran command”, “Wrote file”), an optional detail line (the path, command or url) and right-aligned metadata (exit 0 · 1.2s, 412 B, $0.0021). Machine noise — output chunks, container metrics — is dropped so the rail stays the highlights reel.
- On an issue (
/issues/<id>): once the issue is started, a “Run activity” section appears below the description, scoped to that issue’s runs via the journal’smission_idfilter and narrowed to work entry types (run.*,assignment.*,exec.command,file.written,network.egress,llm.call,keeper.*,summary.generated). It is distinct from the issue’s lifecycle Activity feed below it, and hidden while the issue is still in Backlog or Todo. - On a routine (
/routines): right after you click Run or Test run, the rail shows the just-started run inline so status is visible immediately. - On a run drill-down here: the same component renders the Steps card, so a run reads the same wherever it is opened.
Common pitfalls
- “Agents 4” is a fact about the window, not the workspace. Read the line at the bottom of the rail before quoting a number from this page.
- An empty Agent grouping does not mean nobody worked. Work no routine dispatched is not in the chain index at all.
- A short “what it touched” list is a cut list. The row carries at most five issues and five agents; the exact totals are on the workflow page’s strip.
?pipeline=<slug>narrows, it does not open the routine. To get a routine’s runs, set Group by → Routine and pick it there.- Two crumbs can intersect to the empty set. A run pinned from an event’s spine plus an issue focused in the rail is one click away; the page says the question is empty rather than reporting a quiet system.
- The workflow you picked can vanish. The index is fetched once per look, so a row can be swept between the click and the render. The runs it held are still in the feed.
- A server that records runs in the journal only has no per-run list. The routine runs page says so instead of rendering an empty table.
Related
- Inbox — where waitpoints, escalations and failed runs are actually decided.
- Routines — the definitions whose runs this page streams.
- Crew Journal — the event log Activity reads from, and how to query it programmatically.
- Orchestration — the engine behind the runs (lifecycle, retries, deadlock detection).
- Harbormaster — the approval gates that put a chain into Waiting for you.
- Paymaster — the cost ledger behind the Spend KPI and a run’s cost.