Documentation source
Automations
First-class automation entities with multi-trigger support, sequential multi-step execution, and a "save from chat" workflow for turning agent conversations into repeatable actions.
<Callout type="danger" title="ARCHIVED & DEPRECATED">
**This documentation is obsolete.** As of PR 620, the legacy execution engine has been eradicated. This functionality has been completely replaced by **Tasks** and the Unified Sessions primitive. Do not build new features using these patterns.
Please refer to [Tasks](/docs/features/tasks) and [Response System](/docs/features/response-system) for the new execution architecture.
</Callout>
## Overview
The **Automations** system gives users a discoverable, triggerable inventory of automated work. Each automation is a named **entity** of type `automation` — a record in the standard entity graph, editable in the detail view, relatable to other records, and manageable through the same tooling as every other data type.
The automations page (`/automations`) surfaces three kinds of automated processes in one view:
| Kind | Source | Notes |
|---|---|---|
| Automation entities | `entities` table, `entity_type_slug = 'automation'` | User-defined, multi-trigger, multi-step |
| Agent heartbeats | `agents` table with heartbeat config | Autonomous agent rhythm; managed in Admin > Agents |
| Source syncs | `external_data_sources` table | External data polling; managed in Admin > Sources |
## Key Concepts
### Automation Entity
An automation entity is a named recipe: "when THIS happens, do THAT." It stores:
- **trigger_type** — how the automation fires
- **trigger_config** — JSON with trigger-specific parameters (cron schedule, entity filter, webhook secret)
- **status** — `active | paused | draft`
- **Run history fields** — `last_run_at`, `last_run_status`, `run_count`, `avg_duration_sec`
The execution steps live as **child tasks** parented to the automation's system-owned root task (`slug = "automation-root"`, `entity_id = automation.id`, `is_system = true`). They are NOT stored inline on `content.steps` — see migration `20260411000040_automation_steps_to_tasks.sql` for the one-time backfill that moved legacy inline step data into the tasks primitive.
### Trigger Types
| Trigger | How it fires | `trigger_config` shape |
|---|---|---|
| `manual` | User clicks "Run" | `{}` |
| `cron` | Time schedule (every minute scanner) | `{ "schedule": "0 9 * * 1", "timezone": "America/New_York" }` |
| `entity_created` | A new entity of a given type is created | `{ "entity_type_slug": "prospect", "filter": { "status": "active" } }` |
| `field_changed` | A field on an entity changes value | `{ "entity_type_slug": "prospect", "field": "status", "to_value": "enriched" }` |
| `webhook` | External HTTP POST | `{ "secret": "optional-shared-secret" }` |
### Steps
Each automation has zero or more steps represented as **child tasks** under the automation-root task. Every child task has the usual task surface — `name`, `instructions` (maps to the legacy `prompt`), `agent_slug` (the agent that executes it), and `sort_order` (the authoring order).
```text
tasks
├─ automation-root (is_system=true, parent_id=null, entity_id=<automation id>)
│ ├─ step-1 (instructions="Find new local businesses", agent_slug="lead-finder")
│ ├─ step-2 (instructions="Enrich discovered prospects", agent_slug="lead-finder")
│ └─ step-3 (instructions="Draft outreach emails", agent_slug="lead-finder")
```
Single-step automations have one child. Multi-step chains run sequentially via `triggerTask()` — the session executor creates one child session per child task and marches through them in `sort_order`. Editing, adding, and removing steps goes through the standard task CRUD surface (`POST /api/tasks`, `PATCH /api/tasks/[id]`, `DELETE /api/tasks/[id]`), which is the same surface agents use via the `manageTasks` tool.
### Unified Tasks + Sessions Engine
Automations execute on the **same tasks + sessions infrastructure** as every other execution surface in the product. Each manual run:
- Creates a parent `session` + one child session per step via `triggerTask()` (see `features/actions/server/trigger.ts`)
- Threads through `EntitySessionsPanel` for run history (filtered by `entity_id = automationId`)
- Emits `session_events` rows for realtime UI updates
- Shares one RLS policy set, one status machine, one chat trail with response sessions, tool sessions, and extraction runs
## How It Works
### Execution Flow
```
User clicks Run (or webhook / cron / event trigger fires)
→ POST /api/automations/[id]/run
→ runAutomation(automationId, tenantId):
1. Load automation entity, verify type + status
2. ensureAutomationRootTask(...) — idempotent upsert of the
automation-root task keyed by (tenant, entity_id, slug)
3. SELECT count(*) FROM tasks WHERE parent_id = rootId
→ 422 if zero children (nothing to run)
4. triggerTask({ taskId: rootId, entityId: automationId,
triggeredBy: "manual" })
→ creates parent session + child sessions (1:1 with child tasks)
→ session-executor Inngest function fans out agent sessions
in parallel (or sequentially where depends_on is set)
5. UPDATE entities SET content = {...content,
last_run_at, last_run_status:"running", run_count+1}
→ Response: { status: "started", sessionId, chatId, taskId, stepCount }
```
Sessions flow through the same `EntitySessionsPanel` component as every other entity-scoped execution surface — no automation-specific run UI. Realtime updates are piped through `use-entity-realtime.ts` invalidating `entitySessionsQueryKey`.
### Cron Scanner
Recurring automations are handled by **`task-cron`** — the unified tasks cron scanner. Because automation steps are child tasks under an automation-root task, you set the schedule by configuring `trigger_type='cron'` + `trigger_config.schedule` on the root task. The scanner runs every minute, filters `tasks WHERE status='active' AND trigger_type='cron'`, evaluates each schedule via `shouldRunNow()` (shared with heartbeat), and dispatches via `triggerTask()`. No automation-specific cron logic exists.
### Entity Event Triggers
Entity-event dispatch is handled by **`task-dispatch`** — the unified task event scanner. It listens to `entity/created` and `entity/updated`, queries `tasks WHERE trigger_type IN ('entity_created','entity_updated','field_changed')` scoped by `entity_type_id` or `entity_id`, and dispatches matches via `triggerTask()`. For `field_changed` triggers it diffs `previousContent` against current entity content to confirm the watched field actually changed.
Any entity type can use these triggers — automations are not privileged. Setting `trigger_type='entity_created'` on a task with `entity_type_id=<type id>` makes it fire whenever a new record of that type is created.
### Manual and Webhook Triggers
`POST /api/automations/[id]/run` — user-authenticated manual trigger. Dispatches via `runAutomation()` → `triggerTask()`. Returns `202 { status: "started", sessionId, chatId, taskId, stepCount }`. Error codes: 404 (missing), 400 (not an automation), 409 (paused), 422 (no steps), 500 (db error).
Webhook triggers are handled generically on tasks — a follow-up will wire a `POST /api/tasks/[id]/webhook` endpoint that reads `task.trigger_config.webhook_secret`. The old `POST /api/automations/[id]/trigger` route was deleted in the Phase 7 follow-up because it fired into the void: Phase 6c removed the `automation-run` Inngest consumer but left the producers wired up.
### Save from Chat
Any assistant message in chat has a "Save as Automation" action (Zap icon in the message actions bar). Clicking it opens `SaveAsAutomationDialog` which:
1. Pre-fills the name and description from the message text
2. Lets the user choose a trigger type, cron schedule (if scheduled), and initial status (`draft` or `active`)
3. Creates the automation entity via `POST /api/entities` with `typeSlug: "automation"`
4. Invalidates the `useAutomationEntities` query cache so the automations page updates immediately
## Schema Reference
### `automation` Entity Type (global)
Seeded by `scripts/seed-automation-type.mjs`. `tenant_id: null`, `visibility: "public"` — available to all tenants.
| Field | Type | Description |
|---|---|---|
| `description` | string | Human-readable description of what the automation does |
| `trigger_description` | string | Human-readable trigger label (e.g. "Weekdays at 8 AM") |
| `trigger_type` | enum | `manual \| cron \| entity_created \| field_changed \| webhook` |
| `trigger_config` | string (JSON) | Trigger-specific parameters |
| `status` | enum | `active \| paused \| draft` |
| `output_description` | string | Expected output or side effects |
| `last_run_at` | datetime | Timestamp of most recent execution |
| `last_run_status` | enum | `completed \| failed \| running \| partial` |
| `run_count` | number | Total executions |
| `avg_duration_sec` | number | Average execution time in seconds |
## API Reference
| Endpoint | Method | Auth | Description |
|---|---|---|---|
| `/api/entities?typeSlug=automation` | `GET` | `requireAuth()` | List all automation entities for the current tenant |
| `/api/entities` | `POST` | `requireAuth()` | Create a new automation entity (`typeSlug: "automation"`) |
| `/api/automations/[id]/run` | `POST` | `requireAuth()` | Manually trigger an automation. Rejects `paused` status. Returns `202` with session info. |
| `/api/automations/[id]/toggle` | `POST` | `requireAuth()` | Toggle automation status. |
| `/api/automations/create` | `POST` | `requireAuth()` | Create automation from a parsed NL plan. |
| `/api/automations/parse` | `POST` | `requireAuth()` | Parse a natural-language description into a structured plan. |
| `/api/tasks/[id]/trigger` | `POST` | `requireAuth()` | Manually trigger any task (not automation-specific). |
### Inngest Events
| Event | Fired by | Consumed by |
|---|---|---|
| `ENTITY_CREATED` | Entity create actions | `task-dispatch` (matches tasks with `trigger_type='entity_created'`) |
| `ENTITY_UPDATED` | Entity update actions | `task-dispatch` (matches `entity_updated` and `field_changed`) |
| `SESSION_EXECUTE` | `triggerTask()` + `task-cron` + `task-dispatch` | `session-executor` |
## Frontend Components
### Custom Detail View
The `AutomationDetail` component is registered via `registerEntityDetailView("automation", AutomationDetail)` using the entity detail slug-matched adapter pattern. When a user navigates to an automation entity's detail page, the custom view renders instead of the default bento grid:
- Header with status badge, trigger type badge, and Run button
- Stats row (4 cards): runs, last run, last status, avg duration
- Steps card powered by `<AutomationTasksPanel />` (TaskCard + TaskEditor reuse)
- Trigger configuration panel
- Run history via `<EntitySessionsPanel entityId={...} />` (shared with every other entity)
### `AutomationRunButton`
```tsx
<AutomationRunButton
automationId={entity.id}
status={entity.content.status}
lastRunStatus={entity.content.last_run_status}
/>
```
Disabled when status is not `active` or when `last_run_status = "running"`. Shows a spinner during in-flight requests and shows a toast on success or failure.
### `AutomationTasksPanel`
```tsx
<AutomationTasksPanel automationId={entity.id} />
```
Inline step editor on the automation detail page. Queries `useTasks({ entityId })`, filters to children of the automation's root task, and renders each as a `TaskCard`. Adding and editing steps opens the shared `TaskEditor` sheet from `features/actions/components/task-editor.tsx` — same component used by `/tasks/[id]` — so agents and humans author steps through identical code paths.
### `SaveAsAutomationDialog`
```tsx
<SaveAsAutomationDialog
open={open}
onOpenChange={setOpen}
messageText={messageText}
agentSlug={agentSlug}
/>
```
Triggered from the chat message actions bar. Pre-fills fields from the message text; supports all five trigger types with a cron input for scheduled triggers.
### `useAutomationEntities`
```typescript
const { data: automations, isLoading } = useAutomationEntities();
```
Fetches `/api/entities?typeSlug=automation&limit=100`. Stale time 30 s, refetch interval 30 s so run stats stay roughly current without hammering the server.
## For Agents
Agents can interact with automation entities using the standard entity + task tools:
- `searchEntities` with `entityTypeSlug: "automation"` — find existing automations
- `getEntity` — inspect an automation's metadata, status, and run history fields
- `createEntity` with `typeSlug: "automation"` — create a new automation programmatically
- `updateEntity` — change status (activate/pause), update trigger config or descriptions
- `manageTasks` — **author, edit, and delete step tasks** parented to `automation-root` under the automation entity. This is the same tool agents use to manage extraction tasks, so there is one cognitive model for all executable work.
- `triggerTask` (via server code) — dispatch the automation-root task for a run. Front-end code goes through `POST /api/automations/[id]/run` which wraps `runAutomation()`.
To trigger execution from external systems without a user session, use `POST /api/automations/[id]/trigger` (webhook endpoint, secret-protected).
## Design Decisions
**Automations as entities.** Making automations a first-class entity type means they inherit the full platform: edit in detail view, relate to other records, filter/sort in list views, export via API, appear in the feed, and get managed by agents with `createEntity`/`updateEntity`. No separate CRUD surface needed.
**Steps are tasks, not inline JSON.** Originally the automation schema held a `content.steps` JSON string that was lazy-synced to tasks on every manual run. Two sibling concepts (JSON shadow + task primitive) meant every edit could drift, agents couldn't author steps through `manageTasks`, and the run path had to delete+insert task rows on every dispatch. The 2026-04-11 Phase 7 follow-up collapsed that: tasks are now the source of truth, `content.steps` is gone, and the automation detail page's Steps card is a thin wrapper over the existing task CRUD UI.
**Unified tasks + sessions engine.** Automations use the same `tasks` + `sessions` + `session_events` tables as every other execution surface. Manual run dispatches via `triggerTask()` — the same function called by the `/tasks` page, extraction reruns, and heartbeat agents. Run history displays via `EntitySessionsPanel` filtered by `entity_id = automationId`. There is exactly one observability path.
**Webhook trigger uses automation ID as scope.** Webhook callers don't have user session context. The automation UUID provides sufficient scoping; an optional `trigger_config.secret` adds authentication for sensitive automations.
**Cron scanner skips `last_run_status = "running"`.** Without this guard, a slow automation could accumulate concurrent runs if a cron fires while the previous execution is still in progress. The check is eventually consistent (updated at completion), which is acceptable for minute-granularity schedules.
**Custom detail view via slug-matched adapter.** The `AutomationDetail` component is registered via `registerEntityDetailView()`, following the same pattern as `registerEntityCard()`. The entity detail page checks the registry before rendering the default bento grid. Platform code remains entity-type agnostic.
## Coexistence with Other Automation Systems
| System | Relationship |
|---|---|
| Agent heartbeats | Autonomous agent rhythm — separate from user-defined automations. Both appear on the automations page. Future candidate for migration to automation entities. |
| Extraction tasks | Field-level work for populating entity fields. Same `tasks` + `sessions` primitive as automations — only the `output_type` differs (`field`/`fields` vs. `none`). The two systems now share the same editor, dispatcher, and run history. |
| Source syncs | Polling external data endpoints. Future candidate to become automation entities (post-validation). |
| Tools | Interactive, single-use. Agents in automation steps can invoke tools as part of their execution. |
## Shared Utilities
| Utility | Source | Location | Used by |
|---|---|---|---|
| `AUTOMATION_TYPE_SLUG` | Local | `features/automations/lib/constants.ts` | All automation code |
| `TRIGGER_LABELS` | Local | `features/automations/lib/constants.ts` | UI components, detail view, dialog |
| `parseTriggerConfig()` | Local | `features/automations/lib/constants.ts` | All trigger functions, detail view |
| `cronToHuman()` | `@sprinterai/runtime/automation` | Re-exported via `features/automations/lib/schedule.ts` | Detail view cron display |
| `buildParserSystemPrompt()` | `@sprinterai/runtime/automation` | Re-exported via `features/automations/lib/nl-parser.ts` | NL automation composer |
| `UnifiedAutomation` | `@sprinterai/core/automation` | Re-exported via `features/automations/types.ts` | Automations page, hooks |
| `AutomationRun` | `@sprinterai/core/automation` | Re-exported via `features/automations/types.ts` | Run history hooks |
| `shouldRunNow()` | Local | `features/agents/heartbeat.ts` | Cron scanner (shared with heartbeat) |
### `@sprinterai` Package Usage
The automation feature consumes shared utilities from `@sprinterai/core` and `@sprinterai/runtime` where the implementations are identical. Local files re-export from the packages so all importers use the same paths — no need to import the package directly.
- **`@sprinterai/core/automation`** — `UnifiedAutomation`, `AutomationRun` types. Local `AutomationType` extends the package's type with `"entity"`.
- **`@sprinterai/runtime/automation`** — `cronToHuman()`, `buildParserSystemPrompt()`. `AutomationPlanSchema` is kept local (Zod v4 vs package's Zod v3). `getNextRun()` is a local wrapper around `cron-parser` (package version uses dependency injection).
## Related Modules
- [Tasks](/docs/features/tasks) — the primitive that now owns automation step execution
- [Agent System](/docs/features/agent-system) — agent resolution, execution, prompt building
- [Entity System](/docs/features/entity-system) — entity CRUD, `entity_type_slug`, global entity types
- [Chat](/docs/features/chat) — "Save as Automation" dialog is mounted in the chat message actions bar