Ship unified Automations console
## Original requirement (verbatim, epic body)
> When creating a cron, allo user to pick a different type of target. Currently the target is a project. Other types: node, none (just run a task without a project - we have that impelemented), what else?
>
> When node is a target it runs a task on all selected nodes.
>
> Implement a nice target selector, that allows to quickly add all, remove all, do all except specific, etc. It has to be a very compact and robust control. It must work for both projects, nodes and other targets.
>
> Make sure this is naturally working and improves previous work - triggers, crons, etc. All of these things (triggers, webhooks, crons, etc.) are combined into a unified coherent system that is state of the art and uses best practices (do a web research).
Settled upstream (BA Task 5599 PRD decisions D1-D10; Architect Task 5600 design O1-O5). Do not reopen them: D1 no "None" target; D2 node target = one task per resolved node, pinned, in a node-level non-repo workspace; D3 selections are LIVE rules resolved at each firing; D4 crons + triggers merge into one Automation (When -> Target -> Action); D5 system crons merge too, operator-only with a System badge; D6 selector modes exactly All / None selected / Specific / All except; D7 Task action REUSES the task-create UI; D8 action registry (Task + System function), System function takes no target; D9 migrate in place (ingress URLs, secrets, history, agent verbs keep working); D10 overlap per target. O1 system "Nodes" project visible to org admin/owner (and operators) only; O2 no fan-out cap; O3 Task action adds Attachments + optional Assignee, excludes Dependencies and From Jira; O4 node tasks unprivileged; O5 fix dev docs reach at the root cause.
Epic chain (this breakdown): C2 Node targets API -> C1 Automation core API -> C3 Fire engine API; C4 TargetSelector web; C5 TaskSpecFields web; then C6 Automations UI (this child; needs C1, C3, C4, C5) alongside C8 dev docs reach infra; then C7 reader docs (needs C6, C8).
## This child: C6 Automations UI (web)
The one Automations list, the When -> Target -> Action editor, the runs drawer, the web action registry, nav and redirects (architecture sections 7.2, 7.3), wired to C1's API, C3's firing, C4's `TargetSelector` and C5's `TaskSpecFields`. Owns **every dev-browser oracle** of the PRD, run after C1/C3 have deployed.
Acceptance carried verbatim from the PRD (every one is this child's dev oracle):
- **AC1.1** The Automation editor's Target part offers the types **Projects** and **Nodes** for a Task action, and offers no "None" target type. *Verify:* on dev, open Automations → New, choose When = Schedule and Action = Task, then open Target. Observe exactly two types: Projects and Nodes.
- **AC1.2** Switching the type clears the previous selection, and Save stays blocked until the new type has a valid selection. The editor states this inline and does not silently keep the old IDs. *Verify:* pick 2 projects, switch to Nodes, and observe 0 selected plus a disabled Save with its reason. A component test asserts the same.
- **AC1.3** The same Target part, with the same types, appears for **event** automations (webhook / GitHub / Gmail / Slack). Triggers are no longer single-project. *Verify:* on dev, create a webhook automation targeting 2 projects, POST a test delivery to its ingress URL, and observe 2 tasks filed, one per project.
- **AC2.1** A firing with target Nodes files **one task per resolved node**. Each task is pinned to that node, only that node may claim it, and it runs in a node-level, non-repo workspace. *Verify:* on dev, create a schedule automation targeting 2 nodes with instructions "print `hostname` and disk usage", then press Run now. Observe 2 tasks, each claimed by its own node, each with output naming its own host.
- **AC2.2** Every node state at fire time is decided: An enabled, online node gets its task. An offline node still gets its task. The task waits ready until that node claims it and never runs on another node. A disabled node, or one deleted since the rule was saved, is skipped and recorded as skipped with a reason in the run record. A node whose previous task from this automation is still open is skipped per D10, with reason "previous still open". *Verify:* API tests cover each of these 4 states. On dev, disable one of 2 targeted nodes, press Run now, and observe 1 task plus 1 skip reason in the run detail.
- **AC3.1 One component.** A single selector component serves Projects, Nodes and any future target type through a data adapter, and both the schedule and event editors use it. The trigger and cron editors have no per-surface copies. *Verify:* a code search shows one selector implementation used by the Automation editor for every target type, and the old `ProjectsField` and trigger `<select>` are gone.
- **AC3.3 Live rules (D3).** All and All except are saved as rules, not ID lists. A project or node created after save is included in the next firing, and an excluded item never is. *Verify:* API test: save "All except P1", create P3, fire, and observe tasks for every org project except P1, P3 included. On dev, observe the same.
- **AC3.2 Modes.** It offers exactly All / Specific / All except, plus the empty state (D6), and gives one-action shortcuts: **Select all** (switches to All), **Clear** (empty), and **Except…** (switches to All except, then pick exclusions). *Verify:* on dev, reach each mode in ≤ 2 clicks from any other and observe it.
- **AC3.8 Same control, any target.** The control behaves identically for Projects and Nodes: same modes, summary style and states. *Verify:* run AC3.2–AC3.7 for both types.
- **AC3.6 Resolved preview.** The editor always shows the **resolved count and list** of what the rule selects right now, for example "Will run on 11 of 12 projects". *Verify:* toggle exclusions on dev and observe the count update immediately.
- **AC4.1** One **Automations** list replaces the separate Schedule and Triggers tabs. Each row shows name, When (human-readable: "Every day 09:00 UTC", "GitHub: pull_request opened", "Webhook"), Target summary (AC3.4), Action, status (active / paused / failing), last run and next run (schedules). Rows can be filtered by When kind and status. *Verify:* on dev, the list shows former crons and former triggers together, each with every column filled or showing an explicit "—" with its reason.
- **AC4.2** One editor, with sections **When → Target → Action** in that order. Changing When between schedule and event kinds keeps the Target and Action already entered. *Verify:* on dev, create one of each When kind through the same editor.
- **AC4.3 Task action reuses the task-create UI (D7).** The Action = Task section embeds the same component the console uses to create a task, so every field and validation there is present here. A field added to task creation later appears here with no extra work. *Verify:* a code search shows one task-create form used by both surfaces. On dev, every persona offered in the regular create flow (an epic included) is offered here, and a fired epic automation starts its pipeline.
- **AC4.4 System functions (D5, D8).** Operators see the system activities in the same list with a "System" badge, can pause, resume, Run-now, and edit the schedule, and can re-create a deleted one only where it is `creatable`. Non-operators never see them in the list, the API or counts. *Verify:* on dev as operator, observe the System rows. As an org admin, observe none.
- **AC4.5 Extensible actions (D8).** Action types come from one registry. Adding a type is one registry entry plus its form, with no change to the editor shell or the list. *Verify:* a test registers a dummy action type that the editor renders.
- **AC4.6 Run history is unified.** Each automation shows one Runs view: time, cause (schedule tick / event delivery / Run now), per-target outcomes (filed task link / skipped + reason / error), and, for events, the delivery payload and dedup status as triggers show today. *Verify:* on dev, Run now on a 2-target automation and observe 2 per-target lines linked to their tasks.
- **AC5.1** Every existing user cron becomes an Automation, When = Schedule, Target = Specific (its `project_ids`), and Action = Task with the same persona, instructions, model, autonomous and hidden. It keeps firing at the same times with no missed and no duplicate firing across the deploy. *Verify:* on dev compare each migrated row against its pre-deploy cron, and observe its next scheduled firing land once.
- **AC5.2** Every existing trigger becomes an Automation, When = its source kind/config, Target = Specific [its project], and Action = Task (persona, prompt template). Its **ingress URL and secret are unchanged**, and deliveries already queued are processed. *Verify:* on dev, POST to a pre-existing webhook URL with its existing secret after deploy and observe a task filed.
- **AC5.3** Every system cron becomes a System automation (AC4.4), with its single-instance guarantee intact. *Verify:* a dev list check.
- **AC5.4** Old URLs (`/crons`, `/crons/new`, `/crons/:id`, `/triggers`, `/triggers/new`, `/triggers/:id`, the legacy `?tab=` keys) redirect to the matching Automations page, and never 404. *Verify:* open each on dev.
- **AC5.6** Past run history (`cron_runs`, `trigger_deliveries`) is visible in the Runs view of the migrated automation. *Verify:* on dev, open a migrated automation that had runs before deploy and observe them.
- **AC6.1** Everything above is **on by default on dev**, with no feature flag, and reachable from the main navigation (Automation → Automations) by the same roles that reach Schedule and Triggers today. *Verify:* a fresh browser session on dev as org admin reaches Automations and creates and fires a node-targeted and a project-targeted automation end to end.
- **AC7.1** Missed schedule firings during downtime follow one stated policy, shown in the editor. The default is to **skip missed runs and resume at the next run**. *Verify:* the editor shows the policy text.
- **AC7.2** Overlap policy is per target (D10) and shown in the editor ("Skip a target whose previous task is still open").
- **AC7.4** Every firing is observable: one run record per firing with per-target outcomes (AC4.6). A failing automation shows a failing status on the list (AC4.1).
- Plus the selector's dev keyboard-only pass (AC3.7) and the AC3.5 dev check: add a project with a 40-character name, select it alongside 5 others at 360 px; the name stays identifiable and its tooltip shows it in full.
### How (settled design, implement exactly)
- `apps/web/src/pages/automations/`: **List** - columns Name · When (via `pages/cron/schedule.ts`; events "GitHub: pull_request opened", "Webhook") · Target (`summarizeTarget`) · Action · Status (status_reason tooltip) · Last run · Next run; empty cell "—" with reason (e.g. "—: events have no next run"); filters When kind + status; System badge (operators only). **Editor** - When: segmented Schedule | Webhook | GitHub | Gmail | Slack; Schedule reuses `ScheduleEditor`; event config LIFTED (moved, not copied) from `TriggerEditPage` into `EventSourceFields`; two policy lines under When: "Missed runs while down are skipped; the next run happens on schedule" and "A target whose previous task is still open is skipped". Target: `TargetSelector` (hidden when !takes_target), wired to `POST /api/automations/targets/preview` debounced 300 ms; a one-line notice above 100 targets ("N tasks per run"), save never blocked by size. Action: type picker (`/action-types`) then the registry form. Changing When keeps Target and Action; switching schedule<->event offers the event-only Title template and keeps Body. Staged attachments upload right after the first successful create (`POST /api/automations/{id}/attachments`).
- **Runs drawer** (extend `pages/cron/RunDrawer.tsx`): one row per run (cause, time, outcome); expanding shows per-target lines grouped by outcome with counts ("480 filed · 15 skipped: previous still open · 5 error"), error and skipped groups expanded first, each line linking its task; event runs show delivery preview + dedup status; pre-deploy runs show stored detail.
- **Web action registry** `pages/automations/actions/registry.ts`: `{type, Form, summary(params)}`; entries `task` (`TaskSpecFields context="automation"`, `templateHint` for events, `targetKind` passed), `noop`, `system` fallback (read-only `ActivityParamsForm`); unregistered type renders "No editor for <type>"; a test registers a dummy type the editor renders.
- **Nav/redirects:** `AUTOMATION_TABS` (`apps/web/src/lib/automationTabs.ts:22`, rendered by `pages/workflows/WorkflowsPage.tsx`) becomes Automations · Playbooks; `/crons`, `/crons/new`, `/crons/:id`, `/triggers`, `/triggers/new`, `/triggers/:id` and legacy `?tab=` keys -> `/automations[/new|/:id]`, ids via `GET /api/automations/by-legacy/{cron|trigger}/{id}`; unknown id -> list with "That automation no longer exists", never a 404. System chip on the Nodes project wherever a project name renders (sidebar, breadcrumb, board filter, project list) using C2's `system_kind`.
- **Deletions:** `ProjectsField` (`pages/cron/ActivityParamsForm.tsx:373`), the trigger project `<select>`, the old Schedule/Triggers pages. `ActivityParamsForm` stays only as the read-only system-function params form.
- Dev oracles use `https://app.dev.jaah.ai`. AC5.1: compare the migrated org-b cron "Compliance probe — retired credential reference (Task 2906, Org B)" against C1's pre-merge snapshot (C1's report - find it via `task_search`/`task_read`). AC5.2/AC5.6: find C1's org-b webhook trigger by the name in C1's report, re-reveal the SAME secret via the console's reveal-secret action (NEVER Rotate), POST a signed delivery to `https://hooks.dev.jaah.ai`, observe the task and the pre-deploy delivery in Runs. AC2.1's 2-online-node run uses the **operator** in its own org (org-b has one online node); org-b exercises AC2.2's offline-node case. Per the project instructions: while testing in the UI, note anything broken or inconvenient and file it as itemized suggestions.
Credentials & reach:
- `gmind/skynet/dev-orgb` - tenant org admin on the dev CP - **verified** (console sign-in; Nodes page lists `orgb1` online and `mac-t4743` offline): every "on dev in a browser as org admin" oracle (AC1.x, AC2.2 offline case, AC3.x, AC4.1-4.3, AC4.5-4.6, AC5.4, AC5.6, AC6.1, AC7.x).
- `gmind/skynet/dev` - dev operator (superadmin) - **verified**: AC4.4 System rows, AC5.3, and AC2.1's 2-online-node run.
- Hooks edge `https://hooks.dev.jaah.ai` - **verified** reachable from h2 (AC1.3 / AC5.2 ingress POSTs).
- Use `https://app.dev.jaah.ai`, never `dev.jaah.ai` (internal ALB, no answer from h2) nor the legacy base_url on the logins.
- Pre-deploy fixtures (C1 records them before its merge): the org-b runner cron snapshot and the org-b webhook trigger - **verified** that the means exist.
- Jaah Deploy watch via `jaah-forge` - verified.
Never write a credential value or the trigger secret anywhere.
Out of scope: Do NOT change the API contracts C1/C3 pinned (fix a genuine API bug minimally and name it). Do NOT change `TargetSelector` / `TaskSpecFields` behaviour beyond wiring (fix a genuine defect minimally and name it). Do NOT touch Playbooks beyond the tab position. Do NOT write reader docs (C7). Epic out of scope (PRD section 5, binds every child): No "None" target type for tasks and no projectless-task feature exposed to users (D1). No group, tag, node-group, cloud/self-hosted or online-only selector rules (D6). No new action types beyond Task and System function: no Slack, outbound webhook, email or playbook-start actions (D8). Do not change the webhook ingress URL scheme, existing secrets, signature verification or the dedup rules (D9, AC7.3). Do not change the task-create form's own behaviour to fit automations; any extension must leave regular task creation unchanged (D7). Do not change task claim/placement for non-automation tasks (D2). Do not alter what any system activity does when it runs, only how it is listed and edited (D5). Do not change Automation access roles (AC4.7). Do not rename or remove the agent verbs (AC5.5). No feature flag and no "ships disabled". Deferred, NOT built here (architecture section 9): F-contract; node_projects hygiene for the Nodes project; elevated (root) node tasks; Dependencies and From Jira in automation Task actions; pause-on-failure; a catch-up window.
Done means (execute each on dev after your merge deploys, cite evidence - screenshots, URLs, task ids):
- AC1.1 - Automations → New, When=Schedule, Action=Task, Target shows exactly Projects and Nodes - oracle: dev screenshot.
- AC1.2 - pick 2 projects, switch to Nodes: 0 selected, Save disabled with its reason - oracle: dev screenshot + component test.
- AC1.3 - webhook automation on 2 projects, signed test delivery -> 2 tasks, one per project - oracle: dev task ids.
- AC2.1 - (operator, own org) schedule automation on 2 online nodes, "print `hostname` and disk usage", Run now -> 2 tasks, each claimed by its own node, output names its own host - oracle: dev task ids + outputs.
- AC2.2 - disable one of 2 targeted nodes, Run now -> 1 task + 1 skip reason in run detail; org-b offline node's task waits ready - oracle: dev screenshots.
- AC3.1 - code search: one selector implementation; `ProjectsField` and the trigger `<select>` gone - oracle: grep output.
- AC3.3 - All except P1, create P3, fire -> every project except P1, P3 included - oracle: dev task ids.
- AC3.5 / AC3.7 - 40-char name with 5 others at 360 px stays identifiable with full-name tooltip; keyboard-only pass works - oracle: dev screenshots. A summary that is one line but shows shredded names FAILS.
- AC3.2 / AC3.8 - on dev, for BOTH Projects and Nodes: each mode reached in <=2 clicks from every other, Select all / Clear / Except… work, summary style and states identical - oracle: dev screenshots per type.
- AC3.6 - toggling exclusions updates "Will run on N of M" immediately and the list matches - oracle: dev screenshots.
- AC4.1 - list shows former crons and triggers together, every column filled or "—" with reason, filters work, and a failing automation shows `failing` with its reason (set it up by targeting an org-b project with `fleet_enabled` off and pressing Run now, which records an `error` target; restore the flag after) - oracle: dev screenshot.
- AC4.2 - one schedule and one event automation created through the same editor; When switch keeps Target and Action - oracle: dev screenshots.
- AC4.3 - every regular-create persona (epic included) offered; a fired epic automation starts its pipeline - oracle: dev epic task id + its child stages.
- AC4.4 / AC5.3 - operator sees System rows with badge, can pause/resume/Run now/edit schedule, and New offers only `creatable` system functions; org admin sees none - oracle: dev screenshots for both roles.
- AC4.5 - dummy registry type renders in the editor - oracle: component test.
- AC4.6 / AC7.4 - Run now on a 2-target automation shows 2 per-target lines linked to tasks, grouped by outcome with counts - oracle: dev screenshot.
- AC5.1 - migrated org-b cron matches C1's pre-deploy snapshot and its next firing lands exactly once - oracle: dev screenshots + the run list.
- AC5.2 / AC5.6 - C1's org-b webhook: same secret re-revealed (not rotated), signed POST files a task; pre-deploy delivery visible in Runs - oracle: dev task id + screenshot.
- AC5.4 - each old URL (/crons, /crons/new, /crons/:id, /triggers, /triggers/new, /triggers/:id, legacy ?tab= keys) redirects, none 404 - oracle: dev URL list with landing pages.
- AC6.1 - fresh org-admin session reaches Automation → Automations and creates + fires a node-targeted and a project-targeted automation end to end - oracle: dev screenshots + task ids.
- AC7.1 / AC7.2 - editor shows both policy lines; firing twice with an open first task records "skipped — previous still open" per target - oracle: dev screenshots.
- `./dc check full` green; PR merged to develop; Jaah Deploy for the merge commit succeeds (fix and re-merge if not).
Outcome
Automations console shipped: PR #4946 merged, CI green, Jaah Deploy succeeded. Dev oracles pass as org-b admin. Not verified: AC2.1 and operator half of AC4.4/5.3 (operator sign-in now SSO-only). 10 UI follow-ups in report.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Ready
- Runner started
- Awaiting response
- Awaiting response
- Runner resumed
- Runner ended (done)