diff --git a/apps/web/src/components/app/docs-sections/automations.tsx b/apps/web/src/components/app/docs-sections/automations.tsx
index 78c30a26..c899a968 100644
--- a/apps/web/src/components/app/docs-sections/automations.tsx
+++ b/apps/web/src/components/app/docs-sections/automations.tsx
@@ -51,8 +51,8 @@ export function AutomationsContent() {
Agent type — claude,{" "}
codex, cursor, opencode, or{" "}
terminal. Terminal templates launch a plain shell
- session and skip the prompt, worktree, full access, and media fields
- below.
+ session and skip the prompt, worktree, full access, media, and
+ self-improve fields below.
Model — shown for CLI types with a curated model
@@ -121,11 +121,16 @@ export function AutomationsContent() {
Launching templates
- There are three ways to launch a template:
+
+ There are three ways to launch a template, and all three open the same
+ launch dialog — there is no launch-on-click path, even for a template
+ with no arguments:
+
-
Template detail pane — select a template in the
- sidebar, fill in any arguments, and click Launch.
+ sidebar. It lists the arguments the prompt expects, and{" "}
+ Launch opens the dialog to fill them in.
-
Inline play button — each template in the list has
@@ -167,11 +172,20 @@ export function AutomationsContent() {
A job references a backing template and adds automation on top: cron
scheduling, run timeouts, singleton enforcement, structured reporting
- via MCP tools, auto-archive, and notifications. Jobs are the right
+ via MCP tools, auto-archive, and webhook triggers. Jobs are the right
choice for recurring or monitored work — nightly triage, release
babysitting, janitorial cleanup — where you want a machine-readable
outcome.
+
+ Creating a job also creates a hidden backing template that holds the
+ agent config (prompt, agent type, model, worktree, full access). It
+ stays out of the Templates list and the palette, and the job's own
+ forms are how you edit it. Job runs notify through the ordinary
+ per-agent channels — a job agent's done,{" "}
+ waiting_user, or blocked event reaches Slack
+ and web notifications exactly like any other agent's.
+
@@ -239,15 +253,17 @@ export function AutomationsContent() {
-
- Run timeout, minutes — how long an active run (
-
started / running) can stay open before
- the scheduler force-stops it as timed_out. Defaults to
- 30 minutes.
+ Run timeout, minutes — wall-clock ceiling measured
+ from the moment the run starts, after which the scheduler
+ force-stops it as timed_out. Defaults to 30 minutes. It
+ keeps counting while the run is parked on a question, so raise it
+ for jobs that expect to wait on a human.
-
Wait for input, minutes — how long a run can sit in{" "}
needs_input before being marked timed_out.
- Defaults to 24 hours.
+ Defaults to 24 hours, but the run timeout above applies at the same
+ time and usually expires first.
-
Use worktree — create a fresh git worktree for each
@@ -262,20 +278,30 @@ export function AutomationsContent() {
Keep agent after run completes — by default the
agent is auto-archived once a run reaches a terminal state. Check
this to leave the agent (and its worktree) around for inspection.
+ The job's detail pane then offers an Open session{" "}
+ button to pick up where the run left off.
After creating a job, open its Configure tab to
adjust these settings and options like{" "}
- Webhook trigger — enable it to generate a secret URL
- that fires a run via HTTP POST. No auth header is needed; the secret
- in the URL is the credential. The prompt itself is edited on the job's{" "}
+ Webhook trigger — enable it, hit{" "}
+ Save, and Dispatch generates a secret URL that fires
+ a run via HTTP POST. No auth header is needed; the secret in the URL
+ is the credential. The prompt itself is edited on the job's{" "}
Prompt tab.
+
+ The Enabled switch at the top of{" "}
+ Configure is the exception to the Save button: it
+ writes immediately, and it stays greyed out until the job has a
+ schedule saved. The same tab ends with Remove job,
+ which deletes the job, its schedule, and its run history.
+
- On-demand runs
+ On-demand runs
Every job has a Run now button on its detail pane.
This spawns a run immediately with{" "}
@@ -309,9 +335,11 @@ export function AutomationsContent() {
shape.
- job_needs_input — pause the run and surface a question
- in the UI; the run stays in needs_input until someone
- resumes it or it hits the wait-for-input timeout.
+ job_needs_input — pause the run and surface the
+ question on its History entry. Answer the agent in its own terminal
+ session; the run stays in needs_input until the agent
+ calls a terminal tool or a timeout fires. There is no answer box in
+ the Jobs UI.
@@ -319,7 +347,9 @@ export function AutomationsContent() {
needs_input, completed, failed,{" "}
timed_out, and crashed. A run that exceeds
its run timeout without reaching a terminal tool is force-stopped as{" "}
- timed_out.
+ timed_out; one whose agent session ends first — stopped,
+ errored, or its tmux session gone — is recorded as{" "}
+ crashed.
@@ -365,15 +395,9 @@ export function AutomationsContent() {
dispatch_review_get_feedback,{" "}
dispatch_review_add_message,{" "}
dispatch_review_resolve, and{" "}
- dispatch_review_reopen. See below.
-
-
- Human review —{" "}
- dispatch_review_list_feedback,{" "}
- dispatch_review_get_feedback,{" "}
- dispatch_review_resolve, and{" "}
- dispatch_review_add_message. Read, resolve, and reply
- to review feedback left on the Changes tab.
+ dispatch_review_reopen. The same family covers findings
+ a persona filed and feedback a human left on the Changes tab — read
+ them, reply in the item thread, and set each outcome. See below.
Brain (shared memory) —{" "}
@@ -441,11 +465,20 @@ export function AutomationsContent() {
History and status
Each job in the sidebar list shows its last run's status alongside its
- schedule and enabled state, and the Jobs overview (shown when no job
- is selected) lists upcoming scheduled runs. Open the{" "}
- History tab on a job to browse past runs with their
- start time, duration, trigger source (Manual, Scheduled, or Webhook),
- and expandable report.
+ schedule and enabled state, plus keeps agent and{" "}
+ callable markers where they apply. The Jobs overview (shown
+ when no job is selected) covers the last 7 days: total runs, success
+ rate, average duration and failure count, a daily-runs chart, an{" "}
+ Upcoming list of the next scheduled runs, and a{" "}
+ Recent Activity list that jumps straight to a run.
+
+
+ Open the History tab on a job to browse past runs
+ with their status, start time, duration, trigger source (Manual,
+ Scheduled, or Webhook), and expandable report. An expanded run shows
+ the report summary, each task with its own status, and the last five{" "}
+ job_log lines per task — so write logs assuming only the
+ tail is visible.
diff --git a/apps/web/src/lib/tips/tips.ts b/apps/web/src/lib/tips/tips.ts
index 870c7882..82dbed1d 100644
--- a/apps/web/src/lib/tips/tips.ts
+++ b/apps/web/src/lib/tips/tips.ts
@@ -276,6 +276,14 @@ export const tips: Tip[] = [
since: "0.33.7",
surfaces: ["ambient"],
},
+ {
+ id: "job-webhooks",
+ title: "Fire a Job From Anywhere",
+ body: "A job's Configure tab has a Webhook trigger toggle: save it and you get a secret URL that starts a run on an HTTP POST — no auth header, so CI, a shortcut, or another machine can kick it off.",
+ docsSection: "automations#on-demand-runs",
+ since: "0.22.0",
+ surfaces: ["ambient"],
+ },
{
id: "dispatch-plugin",
title: "Teach Your CLI About Dispatch",
diff --git a/docs/17-jobs.md b/docs/17-jobs.md
index c07419da..13b7b3ad 100644
--- a/docs/17-jobs.md
+++ b/docs/17-jobs.md
@@ -6,7 +6,7 @@ The Automations system has two layers:
- **Templates** — reusable agent launch configurations. A template captures a prompt, agent type, directory, worktree settings, and an optional set of runtime arguments (`{{D:Arg Name}}` with optional filter-style modifiers like `|required|multiline`). Templates can be launched from the Cmd+K command palette or the Automations UI to spin up a normal agent session with no supervision.
-- **Jobs** — automation on top of a template. A job references a backing template and adds scheduling (cron), timeouts, singleton enforcement, structured reporting via MCP, auto-archive, and notifications. Each job invocation is a **run**.
+- **Jobs** — automation on top of a template. A job references a backing template and adds scheduling (cron), timeouts, singleton enforcement, structured reporting via MCP, auto-archive, and webhook triggers. Each job invocation is a **run**. Creating a job auto-creates its backing template with `callable: false`; `GET /api/v1/templates` filters job-backed templates out, so they never show up in the Templates tab or the palette.
Templates are the right choice for quick-launch workflows (code review, feature scaffolding, ad-hoc tasks). Jobs are for recurring or monitored work (nightly triage, release babysitting, janitorial cleanup) where you want a machine-readable outcome.
@@ -16,18 +16,23 @@ Templates are the right choice for quick-launch workflows (code review, feature
Templates are uniquely identified by (`directory`, `name`). Each template has:
-| Field | Description |
-| ------------- | ---------------------------------------------------------- |
-| `name` | Display name, unique within its `directory` |
-| `description` | Optional short description shown in Cmd+K and launch views |
-| `directory` | Absolute path of the repo the template runs against |
-| `prompt` | User-supplied prompt used as the agent's first turn |
-| `agentType` | One of `claude`, `codex`, `opencode` |
-| `useWorktree` | If true, the agent gets its own git worktree |
-| `baseBranch` | Base branch for the worktree (optional) |
-| `branchName` | Branch for the worktree (optional) |
-| `fullAccess` | Pass the agent CLI's full-access/bypass-approvals flag |
-| `callable` | If true, the template appears in the Cmd+K command palette |
+| Field | Description |
+| ------------- | ----------------------------------------------------------- |
+| `name` | Display name, unique within its `directory` |
+| `description` | Optional short description shown in Cmd+K and launch views |
+| `directory` | Absolute path of the repo the template runs against |
+| `prompt` | User-supplied prompt used as the agent's first turn |
+| `agentType` | Any of `claude`, `codex`, `cursor`, `opencode`, `terminal` |
+| `model` | Optional model id, validated against the agent type |
+| `useWorktree` | If true, the agent gets its own git worktree |
+| `baseBranch` | Base branch for the worktree (optional) |
+| `branchName` | Branch for the worktree (optional) |
+| `fullAccess` | Pass the agent CLI's full-access/bypass-approvals flag |
+| `callable` | If true, the template appears in the Cmd+K command palette |
+| `allowMedia` | Default true: show a Context area for files/links at launch |
+| `selfImprove` | Append run-only guidance to revise the saved prompt |
+
+Templates take the full agent-type table (`AGENT_TYPES`); jobs take the CLI subset (`CLI_AGENT_TYPES`), since a terminal agent cannot run a job.
### Runtime Arguments
@@ -49,8 +54,7 @@ If the same argument appears more than once, modifiers are merged. An argument i
Templates with `callable: true` appear in the Cmd+K command palette under a "Templates" group.
-- Templates **without** arguments show a confirmation step — pressing Enter twice (select → confirm) launches immediately.
-- Templates **with** arguments open a launch dialog where you fill in the argument values before launching.
+Selecting one always opens the launch dialog — there is no confirmation-only path, even for templates with no arguments. The dialog carries an agent-type override, a model override for types with a curated catalog, any argument fields, and (when `allowMedia` is on) a Context area for files and links. The inline play button in the Templates list opens the same dialog.
After launch, the agent record is optimistically added to the sidebar cache and the URL navigates to it immediately. The launch endpoint returns the full agent record (matching the create-agent response shape).
@@ -76,22 +80,28 @@ Jobs are uniquely identified by (`directory`, `name`). Each job references a bac
| Field | Description |
| --------------------- | ------------------------------------------------------------------------------------ |
| `templateId` | References the backing template for agent config |
-| `defaultArgs` | Default argument values for scheduled runs |
+| `defaultArgs` | Values substituted into the template's `{{D:...}}` placeholders on every run |
| `schedule` | Cron expression (optional). Jobs without a schedule can still be triggered manually. |
| `timeoutMs` | Max wall-clock for a run (default 30 min) |
| `needsInputTimeoutMs` | Max time a run may sit in `needs_input` (default 24 h) |
| `singleton` | If true (default), only one run can be active at a time |
| `autoArchive` | If true, the spawned agent is auto-archived when the run completes |
| `enabled` | If false, the cron schedule is skipped but manual runs still work |
+| `callable` | Stored flag; renders a "callable" badge in the jobs list and nothing else |
+| `webhookEnabled` | Generates `webhookSecret` on save, exposing `POST /api/v1/jobs/webhook/:secret` |
+| `notify` | Per-job Slack routing. Read-only today — no write path, so it is always null |
-Agent configuration (prompt, agentType, useWorktree, etc.) is read from the backing template at run time.
+Agent configuration (prompt, agentType, model, useWorktree, fullAccess, selfImprove) is read from the backing template at run time; the job's own copies of those columns are legacy fallbacks. `defaultArgs` is settable only through `POST`/`PATCH /api/v1/jobs` — no UI or MCP tool exposes it, and substitution is skipped entirely when the map is empty.
### Run Lifecycle
States: `started` → `running` → (`completed` | `failed` | `needs_input` | `timed_out` | `crashed`).
- The runner creates an agent named `job--` and waits for a terminal MCP call from it (`job_complete` or `job_failed`).
-- A run that calls `job_needs_input` transitions to `needs_input` and pauses. Answering it (via the UI or deleting the run) resumes or ends it; an unanswered `needs_input` times out per `needsInputTimeoutMs`.
+- A run that calls `job_needs_input` transitions to `needs_input` and pauses. The Jobs UI surfaces the pending question on the run's History entry but has no answer box — you reply in the agent's own terminal session and the agent then calls a terminal tool. An unanswered `needs_input` times out per `needsInputTimeoutMs`.
+- `timeoutMs` is checked against the run's start time on every monitor tick, for every active status. A run sitting in `needs_input` is therefore killed by `timeoutMs` first whenever `timeoutMs < needsInputTimeoutMs` — which the 30 min / 24 h defaults guarantee.
+- If the agent session ends before a terminal call (agent `stopped`/`error`, or its tmux session gone), the monitor marks the run `crashed`.
+- Both timeouts are snapshotted into `run.config` when the run is created, so editing the job mid-run does not change the run already in flight.
- Singleton jobs (the default) only allow one active run at a time. Attempting to launch a second run while one is active returns an error.
### Report Shape
@@ -126,13 +136,11 @@ Report size limits: 1 MB total, 100 tasks, 500 logs per task, 10 KB summary and
### Notifications
-Jobs emit their own Slack messages (distinct from the per-agent notifications in [docs/16-notifications.md](16-notifications.md)) on the following events, each with an independent webhook URL list:
+`JobNotifier` is wired into the run-state-change callback and can route a run to Slack on three events — `onComplete` (`completed`), `onError` (`failed`, `timed_out`, `crashed`), and `onNeedsInput` — each with its own channel list, using the same `slack_webhook_url` setting as agent notifications.
-- `onComplete` — run finished with `status: completed`
-- `onError` — run ended as `failed`, `timed_out`, or `crashed`
-- `onNeedsInput` — run is waiting on human input
+**None of it fires today.** The channel lists come from `job.notify`, and no code path writes that column: it is absent from `createJob`'s insert, from `JobConfigUpdate`, from the routes' Zod schemas, and from the MCP `create_job`/`update_job` tools. `buildRunConfig` falls back to three empty arrays, so `getNotifyChannels` always returns none and the notifier returns before sending. Treat this as a data model waiting on a write path, not a shipped feature.
-The job agent's `latest-event` notifications are suppressed so you do not double-notify on completion.
+What does notify is the ordinary per-agent path in [docs/16-notifications.md](16-notifications.md): job agents are not excluded from it, so their `done`, `waiting_user`, and `blocked` events reach Slack and web notifications like any other agent's. Agents can also call `dispatch_notify` directly, which is in `JOB_TOOLS`.
### API
@@ -141,6 +149,7 @@ See [docs/03-api-spec.md](03-api-spec.md#jobs) for the full endpoint reference.
- `GET /api/v1/jobs` / `POST|PATCH|DELETE /api/v1/jobs`
- `POST /api/v1/jobs/enable` / `POST /api/v1/jobs/disable`
- `POST /api/v1/jobs/run` — trigger manually (`{ name, directory, wait? }`)
+- `POST /api/v1/jobs/webhook/:secret` — unauthenticated trigger, rate-limited to 10/min, returns `{ jobId, runId }` and never waits
- `GET /api/v1/jobs/stats` / `GET /api/v1/jobs/history`
### MCP Tools (job-scope only)
@@ -158,11 +167,11 @@ Job agents may also call analytics tools (`get_activity_summary`, `get_feedback_
## UI
-The Automations pane (`/automations`) has a tabbed sidebar with **Templates** and **Jobs** tabs.
+The Automations pane (`/automations`) has a tabbed sidebar with **Templates**, **Jobs**, and **Brains** tabs.
-The sidebar has a tabbed layout with a sliding indicator that animates between tabs. Both tabs use consistent flat-list styling with border separators and a right-aligned Create button.
+Templates and Jobs share a flat-list layout with border separators, a right-aligned Create button, and a sliding indicator that animates between tabs. Brains is a collection-first browser over the repo-scoped Brain.
-- The **Templates** tab lists callable templates with inline play buttons for quick launch. Templates with arguments open a launch dialog; templates without arguments launch directly. Selecting a template shows its detail view with a Launch button and an editable configuration form.
-- The **Jobs** tab lists configured jobs with their latest run status and an Overview dashboard with stats and charts. Drill-down views show run history, per-run reports, and the MCP log stream. A run blocked on `needs_input` exposes an answer box that resumes the agent.
+- The **Templates** tab lists every non-job-backed template — callable or not — with an inline play button that opens the launch dialog. Selecting a template shows its detail view with a Launch button and an editable configuration form.
+- The **Jobs** tab lists configured jobs with their latest run status, and an Overview dashboard (no job selected) with 7-day stats, charts, Upcoming, and Recent Activity. A selected job has **Configure**, **Prompt**, and **History** tabs; expanding a History row shows the run's report, its tasks, and the last five `job_log` lines per task. A run blocked on `needs_input` shows its pending question there — answering happens in the agent's terminal, not in this pane.
Legacy `/jobs` URLs redirect to `/automations/jobs`.
diff --git a/plugins/dispatch/skills/jobs/SKILL.md b/plugins/dispatch/skills/jobs/SKILL.md
index 4227aeb9..7de66e10 100644
--- a/plugins/dispatch/skills/jobs/SKILL.md
+++ b/plugins/dispatch/skills/jobs/SKILL.md
@@ -1,6 +1,6 @@
---
name: jobs
-description: Run agent work on a schedule with structured pass/fail reporting and notifications. Use for recurring or unattended work — nightly triage, release babysitting, cleanup — or when a run's outcome must be machine-readable.
+description: Run agent work on a schedule with structured pass/fail reporting. Use for recurring or unattended work — nightly triage, release babysitting, cleanup — or when a run's outcome must be machine-readable.
---
# Jobs: scheduled and monitored agent runs
@@ -8,7 +8,8 @@ description: Run agent work on a schedule with structured pass/fail reporting an
A job is automation layered on a template. The template supplies the agent
configuration — prompt, agent type, directory, worktree settings. The job adds
everything that makes an unattended run safe: a cron schedule, timeouts, a
-one-at-a-time guarantee, structured reporting, auto-archive, and notifications.
+one-at-a-time guarantee, structured reporting, auto-archive, and an optional
+webhook trigger.
**Template or job?** A template is for work a human launches — quick-start
workflows out of the command palette. A job is for work that runs on its own and
@@ -36,7 +37,7 @@ Jobs are unique per (`directory`, `name`).
| Field | Meaning |
| --------------------- | ----------------------------------------------------------------- |
| `templateId` | The backing template supplying agent config |
-| `defaultArgs` | Values for the template's arguments on scheduled runs |
+| `defaultArgs` | Values for the template's arguments, on every run. HTTP API only |
| `schedule` | Cron expression. Optional — a job without one still runs manually |
| `timeoutMs` | Wall-clock ceiling for a run (default 30 min) |
| `needsInputTimeoutMs` | How long a run may sit waiting on a human (default 24 h) |
@@ -107,15 +108,24 @@ Limits: 1 MB per report, 100 tasks, 500 logs per task, 10 KB summary and error
strings, 5 KB log messages.
`job_needs_input` pauses the run rather than failing it — the right call when a
-decision genuinely requires a human. An unanswered one times out per
-`needsInputTimeoutMs`.
+decision genuinely requires a human. The answer arrives in your terminal like any
+other message; there is no answer box in the Jobs pane, so nothing "resumes" you
+but the human typing.
+
+Do not count on `needsInputTimeoutMs` as your budget for waiting. `timeoutMs` is
+measured from the start of the run and keeps counting through `needs_input`, so
+with the defaults (30 min vs 24 h) a parked run is killed by the run timeout
+first. Raise `timeoutMs` on any job that expects to wait on a person.
## Notifications
-Jobs send their own Slack messages, with independent webhook lists per event:
-`onComplete` (finished successfully), `onError` (failed, timed out, or crashed),
-and `onNeedsInput` (waiting on a human). The job agent's own per-agent
-notifications are suppressed so completion does not notify twice.
+There is no per-job notification config to wire. The `notify` column exists in
+the data model, but nothing writes it — not the routes, not `create_job`, not
+`update_job` — so the per-job Slack path never fires.
-Wire `onError` and `onNeedsInput` at minimum. A job whose failures are silent is
-worse than no job — it looks like coverage without providing any.
+Job agents are not excluded from the ordinary per-agent notifications, so a run's
+`done`, `waiting_user`, and `blocked` events already reach whatever Slack and web
+notifications the user has configured. That is the coverage you get for free —
+which makes emitting an honest terminal `dispatch_event` the thing that actually
+determines whether a failure is visible tonight. Call `dispatch_notify` directly
+when a run needs to say something the status event cannot carry.
diff --git a/plugins/dispatch/skills/templates/SKILL.md b/plugins/dispatch/skills/templates/SKILL.md
index 81ee72b1..f74408d0 100644
--- a/plugins/dispatch/skills/templates/SKILL.md
+++ b/plugins/dispatch/skills/templates/SKILL.md
@@ -34,13 +34,20 @@ omits prompt bodies — call `get_template` for the one you actually want.
| `description` | Shown in Cmd+K and launch views |
| `directory` | Absolute path of the repo this template runs against |
| `prompt` | The agent's first turn |
-| `agentType` | `claude`, `codex`, `cursor`, or `opencode` |
+| `agentType` | `claude`, `codex`, `cursor`, `opencode`, `terminal` |
| `model` | Optional model id, matching the agent type |
| `useWorktree` | Give the agent its own git worktree |
| `baseBranch` | Base branch for that worktree |
| `branchName` | Branch name for that worktree |
| `fullAccess` | Pass the CLI's full-access / bypass-approvals flag |
| `callable` | Show it in the Cmd+K command palette |
+| `allowMedia` | Default true: offer files and links at launch |
+| `selfImprove` | Let the agent revise this saved prompt after a run |
+
+Templates take the full agent-type table, jobs only the CLI subset. A `terminal`
+template opens a plain shell, so launching one ignores the prompt (and its
+arguments and self-improve guidance), the worktree settings, `fullAccess`, and
+any startup files.
Set `useWorktree` for anything that writes code. Agents sharing a working tree
overwrite each other's changes, and the damage is silent.
@@ -72,13 +79,16 @@ names it expects.
## The command palette
-Templates with `callable: true` appear in Cmd+K under "Templates":
-
-- **No arguments** → a confirmation step; Enter twice launches it.
-- **With arguments** → a launch dialog for filling values in first.
+Templates with `callable: true` appear in Cmd+K under "Templates". Selecting one
+always opens the launch dialog — argument fields when the prompt has them, plus
+an agent-type override, a model override, and a Context area when `allowMedia` is
+on. There is no launch-on-Enter path, so a template with no arguments still costs
+the human one extra confirm.
Set `callable: false` for templates that exist only to back a job — otherwise the
-palette fills with entries nobody launches by hand.
+palette fills with entries nobody launches by hand. (Templates that Dispatch
+creates to back a job are already excluded from both the palette and the
+Templates list.)
## Writing the prompt