Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
18 KiB
status, created, owner
| status | created | owner |
|---|---|---|
| draft | 2026-07-31 | nova28 |
Automation Runs
Why
An automation that produces a report — a nightly drift sweep, a dependency audit — is read daily and edited almost never. Its output was reachable only through the automation's own edit form: Settings → workspace → Automations → open the automation → scroll past the whole configuration → expand a collapsed section → click a row. Six steps into an editing surface to perform a reading task.
The first correction lifted the runs out into a flat cross-automation feed. That was half the fix. Runs have an owner — the automation that produced them — and the question people actually ask is "how is this automation doing", so the primitive is the automation and its history, with the flat feed as a lens over the top rather than the front door.
What
The automation is the object; its runs are its history. The surface is a list and a detail, not a flat feed.
Navigation
- The sidebar carries an Automations section directly under New Task, listing the workspace's automations by name with the same health dot the list uses. The list IS the navigation: picking one opens its history, not a settings form. The section header opens the full list, which adds the next firing and what each one last said — more than a sidebar row should try to hold.
- There is no separate "Runs" nav row, and the destination is named for the object
it lists:
/automations, matching the sidebar section and the settings page, with the sameIconBolt./runsstill resolves so shared links keep working. Two entries pointing at the same place is what made the destination hard to place: the automation is the object, so the nav lists automations. - The section fetches only while it is on screen — a collapsed rail or a folded section issues no requests, since the sidebar is mounted on every page.
/automations — the agenda, not an index
The sidebar already lists automations by name with a health dot, so this page SHALL NOT be a second list of names. It answers the two questions neither the sidebar nor any single automation's page can:
- Up next — every automation ordered by when it fires, soonest first. The time leads the row; the name is secondary, because the name is what the sidebar already gave you.
- The time is the resolved next firing (
~00:00 tomorrow · GMT+8) or the reason there isn't one. The tilde is deliberate: the scheduler ticks on an interval, so the time is approximate and the UI SHALL NOT imply a precision it does not have. - Reasons an enabled automation will not fire, each shown in place of a time: a
run is still open and
max_concurrent_runsis reached; no schedule is set; the trigger itself is switched off. An automation that will not fire stays in the agenda holding its reason — dropping it would make the agenda look complete when it is not. - A standing constraint sits under the agenda, which is exactly what it qualifies: scheduled automations only fire while kandev is running.
- Recent runs — the cross-automation feed inline, newest first, each entry naming its automation. "Did anything go wrong overnight" is the other half of why someone opens this page; behind a link, they would have to ask for it twice. The filtered lens stays one click away for digging.
Detail — /automations/<automation-id>
An automation is one conversation that recurs. The detail view is that conversation, not a log about it.
- The pane shows a run's transcript, at full size, and lands on the newest — "what did it say last night" is the question this page exists for, and it should cost zero clicks.
- The automation's standing instruction is pinned above the transcript, collapsed. It is the same on every run, so it belongs to the automation rather than to any one of them; it is what makes the turns underneath legible.
- A composer is always present. The run is repliable and the agent continues in the same session and worktree, so the reply belongs here rather than on a page the reader has to be sent to.
- Runs are a rail, not the page — a switcher over instances of the same
conversation, grouped Running / Completed, each row a time and a state and
nothing else. The selected run lives in the URL (
?run=<id>): "the run that failed overnight" is a thing people link each other to. - A run that produced no conversation (a skipped firing) is listed but inert. It has nothing to read, and offering it would render an empty pane that looks like a broken link.
- Configuration is behind a
Detailslink in the rail, not a tab beside the transcript. A tab pair claims the two are done about equally; an automation is configured once and read continuously. - A run in flight shows its live output as it happens, not a placeholder.
- A run displayed as Running moves into Completed without a manual page reload once its terminal result is available, including when the health summary temporarily reports no open runs.
- The URL is stable and bookmarkable — checking one automation daily is a first-class use, and it should cost one click from a bookmark.
- Run now fires the automation from the reading surface, because the alternative — waiting until tomorrow to find out whether a schedule works — is not one. A trigger the concurrency cap turns away SHALL be reported as skipped with its reason, never as a fire: nothing ran, and no run appears under Running.
Entries
- Each entry leads with what the run said, not with its status. Kandev's runs are prose written by an agent, so the outcome text is the deliverable; status is metadata. This is the difference between a run log and a CI job table.
- An entry shows: relative time, up to two lines of outcome, and status. In the cross-automation view it also carries the automation's name.
- Outcome text is the run's
error_messagewhen it has one, otherwise itssummary(the tail of the agent's last message). A failed or skipped run therefore explains itself in place. A skipped run is not styled as a failure: the concurrency cap turning a firing away is the cap working, and rendering the reason in red makes a correctly throttled automation look broken. - A run that never produced a task SHALL NOT be clickable.
Relationship to the board
- No automation run appears on the kanban or in the task list, regardless of how it was configured (
automations-settings.md). This surface is where every run is visible. - The flat cross-automation feed keeps the run vocabulary and its history icon — it genuinely is a list of runs. Everything else is named for the automation.
Data model
No new persistent state. The feed reads existing automation_runs rows joined to automations for attribution.
| Field | Source | Note |
|---|---|---|
all AutomationRun fields |
automation_runs |
including the read-time-derived status |
session_id |
task_sessions |
the run's primary session; the transcript mounts from it, so resolving it in the projection avoids a lookup per run on the client. Empty when the run never started one |
automation_name |
automations.name |
INNER join — a run whose automation is gone is unattributable |
summary |
task_session_messages |
tail of the agent's last message, truncated server-side to 280 chars |
The status shown is derived at read time, not stored: a task_created run whose task was deleted or whose primary session was cancelled reads as cancelled, and one whose task was archived reads as archived. This derivation is defined once and shared with the per-automation log — the two views MUST NOT be able to disagree about the same run.
The summary lookup filters task_session_messages by task_id, which requires an index leading with that column (idx_messages_task_author_created). Without it the lookup falls back to a global created_at scan once per row.
API surface
Four actions. The detail view reads one automation's runs and its summary; the list reads every automation's summary; the flat lens reads the workspace feed.
automation.runs.list
request { automation_id: string (required), limit?: number }
response AutomationRun[]
The cross-automation action, used by the list and the flat lens:
automation.runs.list_workspace
request { workspace_id: string (required), limit?: number }
response { runs: WorkspaceAutomationRun[] }
The health actions, one row per automation that has ever run:
automation.summaries
request { workspace_id: string (required) }
response { summaries: AutomationSummary[] } // { automation_id, open_runs, last_run? }
automation.summary
request { automation_id: string (required) }
response { summary: AutomationSummary | null } // null = it has never run
Both facts are answered in ONE statement. Two queries are two snapshots: a run
created between them reads as a still-open last_run with open_runs = 0, so
the row renders idle and the client never starts polling — permanently stale
until someone refreshes by hand.
Every run query orders by created_at DESC, id DESC. Without the tie-break, two
runs written in the same second can order one way in the feed and the other way
in the summary, so a row's "what it last said" would contradict the entry
leading that automation's own activity.
The list SHALL NOT derive an automation's health from the workspace feed. That
feed is capped, so past the cap a quiet automation's newest run falls out of the
window and its row would report "No runs yet" and idle — the two claims a health
indicator must never get wrong — while the open count backing "won't fire —
still running" silently drops to zero. open_runs is counted by the same
definition max_concurrent_runs uses, so the reason shown and the cap causing it
cannot disagree. An automation with no summary row has genuinely never run.
limitdefaults to 50 and is capped at 200. The bound is enforced in the store, so it holds for every caller rather than only the handler.- Runs are ordered by
created_atdescending across all of the workspace's automations. - An empty result serializes as
[], nevernull. - A missing or empty
workspace_idreturnsBAD_REQUEST.
Permissions
The action is workspace-scoped: the caller must be authorized for workspace_id by the same check that gates automation.list. A run belonging to another workspace SHALL NOT appear in the feed, regardless of the requested limit.
Failure modes
| Condition | Behaviour |
|---|---|
| Workspace has never run an automation | Empty state that names the next action, not a bare "no results" |
| A filter excludes everything | Distinct message from the never-run case, so the user knows a filter is on |
Run has no task_id (e.g. a skipped run) |
Entry renders and explains itself; it is inert, not a dead link |
Run has no summary (agent never spoke) |
Entry says why there is nothing to read — still running, or no report recorded. It MUST NOT fabricate an outcome, but a blank line that leaves the reader guessing is not the alternative |
| The list request fails | Surface the failure and offer a retry. A transient failure MUST NOT leave a permanently empty feed |
| A slow first load resolves after a later refresh | The later response wins; a stale response MUST NOT overwrite fresher data |
| An automation is enabled but cannot fire | The list row states why in place of a next-run time, rather than showing a time that will not happen |
| kandev was not running when a schedule was due | The missed firing runs once on the next tick, not once per missed occurrence. The list's standing note tells the user this can happen |
| Workspace changes while the list is open | Rows from the previous workspace are never shown under the new one, not even for one frame |
| Workspace changes while a detail page is open | The page leaves for /automations. An automation belongs to one workspace, so continuing to show it under a sidebar that says otherwise is a lie about where the user is |
| A run finishes while the page is open | The page stops calling it running without a reload. Polling runs only while something is open, so an idle workspace issues no repeat requests |
| A visible run is still reported as open while the health summary says there are no open runs | The detail rail/drawer continues reading until the visible run receives a terminal status, then moves it to Completed without a reload |
| An open run falls outside the page's own run window | The open count comes from the server, not from the loaded window, so the page still reports work in flight and keeps polling |
Scenarios
- GIVEN a workspace with two automations, WHEN the user opens
/automations, THEN each automation appears as one row carrying its state, its next run (or why it has none), and what it last said. - GIVEN an automation whose only open run has not finished and whose
max_concurrent_runsis 1, WHEN the list renders, THEN its row says it is paused and why, in place of a next-run time. - GIVEN an automation with no cron expression set, WHEN the list renders, THEN its row says it will not run on its own.
- GIVEN any automation, WHEN the user opens its row, THEN the app navigates to
/automations/<automation-id>and lands on Activity, not on configuration. - GIVEN an automation with one run in flight and several finished, WHEN the user opens its detail, THEN the running one is visible without applying a filter, above the finished ones.
- GIVEN an automation detail is open, WHEN the user follows Details, THEN the editor appears, the runs rail stays alongside it, and the URL still identifies the same automation.
- GIVEN an automation with several runs, WHEN the detail opens, THEN the newest run's transcript is on screen without the user selecting anything.
- GIVEN a link carrying
?run=<id>, WHEN it is opened, THEN that run's transcript is shown rather than the newest. - GIVEN a requested run that is no longer in the window, WHEN the page loads, THEN it falls back to the newest rather than rendering an empty pane.
- GIVEN a finished run, WHEN the user types in the composer, THEN the agent continues in the same session and worktree.
- GIVEN a run is in flight, WHEN the user opens it, THEN they see the agent's output as it arrives rather than a placeholder.
- GIVEN a run whose agent asked a question, WHEN the user opens it, THEN they can reply and the agent continues.
- GIVEN a workspace with runs from two different automations, WHEN the user opens the flat lens, THEN all runs appear in one feed ordered newest first, each labelled with its own automation's name.
- GIVEN a run whose agent reported "Sweep complete across all 32 specs", WHEN the feed renders, THEN that text is visible on the entry without opening anything.
- GIVEN a run skipped by the concurrency cap, WHEN the feed renders, THEN the entry shows the skip reason as its outcome and status
Skipped. - GIVEN a run with a
task_id, WHEN the user clicks its entry, THEN the app navigates to/tasks/<task_id>. - GIVEN a run with no
task_id, WHEN the user clicks its entry, THEN nothing navigates. - GIVEN runs of mixed status, WHEN the user selects the
Failedfilter, THEN only failed runs remain listed. - GIVEN runs from automations A and B, WHEN the user filters to automation A, THEN only A's runs remain and B is still offered as an option.
- GIVEN another workspace has newer runs than this one, WHEN the feed loads, THEN none of them appear.
- GIVEN a workspace with no runs at all, WHEN the feed loads, THEN the empty state invites the user to create an automation.
- GIVEN any automation fired, WHEN the user looks at the kanban or the task list, THEN its task does not appear in either —
/automationsis where every run is visible. - GIVEN an automation run finished successfully, WHEN the user opens it and replies, THEN the agent continues in the same session and worktree rather than reporting that the session has ended.
- GIVEN an automation whose newest run is older than the workspace feed's cap reaches back, WHEN its row renders, THEN it still shows that run's outcome rather than "No runs yet".
- GIVEN a run is in flight, WHEN the user leaves the page open, THEN it stops reading "Running" once the run finishes, without a reload.
- GIVEN the detail rail or drawer shows a run as Running while the health summary reports zero open runs, WHEN that run receives a terminal status, THEN the run moves to Completed without the user reloading the page.
- GIVEN an automation with an open run older than its own capped run window, WHEN its detail page renders, THEN it still reports that something is running and keeps polling, because the count comes from the server rather than from the window.
- GIVEN the user switches to another workspace while a detail page is open, WHEN the switch lands, THEN the page leaves for
/automationsrather than showing another workspace's automation. - GIVEN a run still in progress with no summary yet, WHEN the feed renders, THEN the entry says it is still running rather than showing blank outcome text.
Out of scope
- Re-running or retrying a run from the feed. Re-fire is the automation's own Run action.
- Retry-from-step, as n8n offers. Kandev runs are a single agent turn, not a node graph.
- A cross-workspace feed.
/automationsfollows the active workspace. - Removing the flat cross-automation feed. It is demoted from front door to lens, not deleted — with many automations "what happened overnight" is a real question a per-automation view cannot answer.
- A calendar of upcoming firings. The list's next-run cell is the first cut; a forward agenda is a later step.
- Pagination or infinite scroll. The capped limit is the first cut; revisit when a workspace outgrows it.
- Deleting runs from the feed. That stays in the per-automation log, where the delete-all confirmation already lives.