---
title: Automations & Auto-replies (auto-respond to messages) (part 2)
---

# Automations & Auto-replies (auto-respond to messages) (part 2)

## What it is

This is part 2 of the [Automations & Auto-replies (auto-respond to messages)](automations-and-auto-replies.md) page. It carries the auto-response badge taxonomy that names which automated path produced a system message, the per-step breakdown the activity log shows for v2 runs, and the implementation map of the engines, stores, watchers and IPC that execute both kinds of rule.

## Where to find it

Automations and Auto-replies both live in the Automations virtual project in the Omniscio sidebar, next to Quick Replies, with each rule edited inline in its list row; [Automations & Auto-replies (auto-respond to messages)](automations-and-auto-replies.md) walks that surface end to end, including the rule editor, the six auto-reply actions, the action chain, backtest and the sender-authentication rules. The badges and internals on this page are read from a session transcript or from the code paths named below.

## How it behaves

### The auto-response badge — which automated path replied

Every system-injected message — an outgoing operator message Omniscio produced on its own, rather than one you typed — carries a small **auto-response badge** inline. The badge names _which_ of the eleven automated paths produced the message, so a glance at the transcript tells them apart:

| Badge label                      | What produced the message                                           |
| -------------------------------- | ------------------------------------------------------------------- |
| `scheduled wake-up`              | A `ScheduleWakeup` tool call fired after its delay elapsed          |
| `auto-continue`                  | Omniscio nudged the agent to keep going after it paused mid-task    |
| `rate-limit retry`               | A rate-limit window cleared and the session auto-resumed            |
| `auto-resume`                    | Omniscio recovered the session after a crash or a restart           |
| `auto-reply`                     | A user-defined Away Mode / Auto-replies rule fired                  |
| `auto-approved`                  | The dev pipeline auto-approved a gate on your behalf                |
| `auto re-check`                  | The waiting detector re-checked after the agent went quiet mid-wait |
| `Sent by Inbox Pilot` (bot icon) | Inbox Pilot's router chose and sent the reply                       |

Every label except `Sent by Inbox Pilot` uses the **amber** auto-response palette so user-driven and recovery flows read as one family; `Sent by Inbox Pilot` keeps its own surface-grey pill with a bot icon. Auto-response messages persisted before this taxonomy existed fall back to a bare amber `auto` pill. The waiting detector's other auto check-ins are registered kinds too, but render differently: the pre-inbox and inbox **auto check-in** show the ember **⏱ Checked in** tag on the check-in message (see [waiting-detector.md](waiting-detector.md)) instead of this badge, and the ambiguous-wait **confirm** probe stays hidden.

Two of these labels render as a **visible user-message bubble** rather than being hidden by default — `auto-reply` and `auto-approved` — because you opted into the automation that sent them, so the transcript reads like you replied yourself. (The mechanical recovery / continue / wake-up kinds stay hidden unless you turn on _Show system messages_.)

**Hover** any badge for a tooltip that explains _why_ the system spoke. The tooltip is the kind's description plus an optional per-message reason — for example `Auto-injected to continue the agent after a pause — Auto-continue 2/5 — agent stopped mid-task`, or `Auto-injected after a rate-limit cleared — account-switch trigger`. `Sent by Inbox Pilot` additionally shows the decision id (`— decision abc-123`) so you can find the matching row in the AI Coaching decisions log. `auto-reply` keeps the richer rule tooltip — the matching condition and reply source, e.g. `Auto-replied — keyword "subagent" — snippet "Standard reply"`.

**Click** is wired for the `auto-reply` badge only: it jumps straight to the rule in the **Automations** virtual project (under the **Auto-replies** category) with the editor expanded. If the rule has been deleted since it fired, the click shows an "Auto-run rule no longer exists" toast and stays on the message; if the lookup IPC fails, you get an error toast instead and no navigation happens. Every other badge is informational — a non-interactive label, not a button.

Outgoing channel replies (SMS, Slack, Telegram, Gmail) sent by Automations rules still show the bare "auto" badge with no attribution. That follow-up is tracked in [/docs/plans/2026-04-26-automations-channel-reply-attribution-followup.md](../plans/2026-04-26-automations-channel-reply-attribution-followup.md).

**How it works:** the eleven-kind taxonomy lives in one shared module — [/src/shared/auto-response-metadata.ts](/src/shared/auto-response-metadata.ts) — which exports the kind union, a descriptor table (badge label + tooltip text), and the `resolveAutoResponseKind()` / `getAutoResponseReason()` accessors. Each producer stamps its metadata as it writes to the session: `{ systemInjected: true, kind, reason }` for the recovery / schedule / continue kinds, `{ source: 'ai-manager-respond', decisionId, reason }` for Inbox Pilot, the richer `{ autoRule: { ruleId, conditionType, conditionValue (truncated to 80 chars), snippetLabel } }` snapshot for auto-reply rules, or `{ kind: 'pipeline-approval', pipelineAutoAdvance: { gate } }` for a dev-pipeline gate auto-approval — threaded through `writeToStdin` (or `sendMessageToSession` for non-Claude engines) → `addOutput` → `SESSION_OUTPUT` push event → `conversation_messages.metadata`. Which kinds render as a visible user bubble vs. stay hidden is decided by `USER_AUTHORED_AUTO_RESPONSE_KINDS` in [/src/shared/real-conversation-turns.ts](/src/shared/real-conversation-turns.ts). The renderer's [/src/renderer/src/components/ui/AutoResponseBadge.tsx](/src/renderer/src/components/ui/AutoResponseBadge.tsx) resolves the kind through that module, picks the palette + label, formats the tooltip via [/src/renderer/src/components/ui/auto-response-tooltip.ts](/src/renderer/src/components/ui/auto-response-tooltip.ts), and — for `auto-rule` — calls `AUTO_RUN_GET_RULES` to verify the rule still exists before deep-linking through [/src/renderer/src/features/automations/navigate-to-rule.ts](/src/renderer/src/features/automations/navigate-to-rule.ts) (sets `useSessionStore.activeProjectId = AUTOMATIONS_PROJECT_ID`, switches the automations-category-store category to `auto-replies`, and writes `pendingExpandRuleId` so [/src/renderer/src/features/automations/components/AutoRepliesListView.tsx](/src/renderer/src/features/automations/components/AutoRepliesListView.tsx) expands the matching row once it mounts and rules finish loading).

### What the activity log shows for v2 runs

The **Activity Log & Statistics** collapsible at the bottom of the Automations settings panel lists every recent automation run. Click a row to expand it. For runs produced by the v2 chain executor (rules whose `engineVersion === 2`), the expanded panel surfaces a per-step breakdown: each action's label (Archive / Notify / Auto Reply / …), any free-text **detail** the action returned (e.g. `archived 3 threads`), a **dry-run** badge when the step was rendered for backtest only, and a human-readable duration (`45ms`, `1.2s`, `2m05s`). Failed steps show a red ✗ glyph and the underlying error text is hosted in a tooltip on the row so it doesn't push the layout around. Legacy v1 runs keep the single-line outcome (Success / Error + actionError paragraph) — only v2 runs grow the per-step block.

## For agents

### How it works

The **Automations virtual project** is registered via `AUTOMATIONS_PROJECT_ID = '__automations__'` in [/src/shared/types.ts](/src/shared/types.ts); the Dashboard routes that sentinel through [/src/renderer/src/features/dashboard/Dashboard.tsx](/src/renderer/src/features/dashboard/Dashboard.tsx) into the same regular-project layout used by every filesystem project. [/src/renderer/src/features/dashboard/SessionsSidebar.tsx](/src/renderer/src/features/dashboard/SessionsSidebar.tsx) detects the sentinel and renders [/src/renderer/src/features/automations/components/AutomationsSidebarSections.tsx](/src/renderer/src/features/automations/components/AutomationsSidebarSections.tsx) above the standard SESSIONS / SNOOZED / ARCHIVED stack — two collapsibles (RULES + AUTO-REPLIES) backed by `rulesExpanded` / `autoRepliesExpanded` booleans + a `category: 'rules' | 'auto-replies'` slot in [/src/renderer/src/stores/automations-category-store.ts](/src/renderer/src/stores/automations-category-store.ts). The Auto-replies list-view filter is backed by [/src/renderer/src/stores/auto-replies-filter-store.ts](/src/renderer/src/stores/auto-replies-filter-store.ts) — a small Zustand store that holds the substring `query`, plus `matchCount` / `totalCount` published by the list view so the sidebar header can render the `(M of N)` count format without re-running the filter. The main panel is [/src/renderer/src/features/automations/AutomationsProjectView.tsx](/src/renderer/src/features/automations/AutomationsProjectView.tsx); it watches `useSessionStore.activeSessionId` and, when that points at a session belonging to the Automations project, mounts the full `SessionPanel` (the standard close button clears `activeSessionId`). Otherwise it dispatches by category to [/src/renderer/src/features/automations/components/RulesListView.tsx](/src/renderer/src/features/automations/components/RulesListView.tsx) or [/src/renderer/src/features/automations/components/AutoRepliesListView.tsx](/src/renderer/src/features/automations/components/AutoRepliesListView.tsx). AI edit sessions are real rows under SESSIONS — no separate AI-sessions category, no bespoke "back" pill. Persistence: Auto-replies rules live in the `away_mode_rules` SQLite table (intentionally not renamed — the table + IPC channels keep the legacy `AUTO_RUN_*` / `away_mode_rules` names so migrations don't break existing installs), backed by CRUD in [/src/main/db/queries-auto-run.ts](/src/main/db/queries-auto-run.ts) and matched by [/src/main/services/automation/legacy/auto-run-engine.ts](/src/main/services/automation/legacy/auto-run-engine.ts); Automations live in the separate `automation_rules` table, backed by [/src/main/db/queries-automations.ts](/src/main/db/queries-automations.ts) and executed by [/src/main/services/automation/legacy/automation-service.ts](/src/main/services/automation/legacy/automation-service.ts), with renderer state in [/src/renderer/src/stores/automation-store.ts](/src/renderer/src/stores/automation-store.ts). When a message lands, the channel adapter hands it to both engines; Automations runs its ordered rule list first (so its AI condition + action chains take priority), then Auto-replies fill in if nothing matched. Condition evaluation covers literal operators (via [/src/main/services/pattern/pattern-matching.ts](/src/main/services/pattern/pattern-matching.ts)) and, for AI conditions, a structured-JSON classifier with OAuth-vs-API-key dual-path routing. **Cross-engine session output:** an Auto-reply rule fires on a session's finished turn for **every** engine — Claude evaluates it in its NDJSON turn gate ([/src/main/process/ndjson-auto-disposition.ts](/src/main/process/ndjson-auto-disposition.ts)), and non-Claude sessions (Codex / Pi / OpenClaw / Gemini / Cursor / OpenCode / …) evaluate the SAME rules on each clean turn-complete via the engine's `runForExternalTurn`, delivering a **reply** through the unified [/src/main/services/session/session-send.ts](/src/main/services/session/session-send.ts) path (the Claude-only stdin path can't reach an external engine); the other five actions are engine-agnostic.

**Organize / per-session color:** the **Organize the session** action is the `triage` arm of the auto-run action union — `{ type: 'triage', title?, tagId?, color? }` (at least one set), applied independently and best-effort by [/src/main/services/automation/legacy/auto-run-engine.ts](/src/main/services/automation/legacy/auto-run-engine.ts) (title via the shared rename helper, tag via `applyTagToSessionUnscoped(sid, tagId, 'ai')`, color written to the session). It returns `false`, so organizing a session doesn't suppress the `needs_you` transition. The color itself lives in a nullable `color TEXT` column on the `sessions` table (a `#rrggbb` hex or NULL = inherit the status color); the manual **Set color** menu writes it through the `SESSION_SET_COLOR` (`session:set-color`) IPC channel, and either path emits a `SESSION_COLOR_CHANGED` push so every open window re-renders the colored bar live.

**Session-state triggers** are evaluated by a separate poller — [/src/main/services/session/session-state-watcher.ts](/src/main/services/session/session-state-watcher.ts) — that ticks every 30 seconds. On each tick it: (1) lists all enabled `on_session_state` automations, (2) resolves each rule's scope to the matching live sessions (excluding archived/soft-deleted) via [/src/main/services/session/session-scope-evaluator.ts](/src/main/services/session/session-scope-evaluator.ts), (3) computes whether **every** scoped session is in one of the matched statuses, and (4) hands the result to a pure state machine ([/src/main/services/session/session-state-machine.ts](/src/main/services/session/session-state-machine.ts)) that returns one of `fire | record | noop`. The watcher only fires on a **transition** from "not all matched" → "all matched"; on the very first tick after enabling a rule it always records a baseline rather than firing, so an automation enabled while the condition is already true won't surprise you with an immediate fire. Action chains for state triggers receive `{{trigger.matchedSessionIds}}`, `{{trigger.matchedSessionCount}}`, `{{trigger.firedAtIso}}`, and `{{trigger.scopeDescription}}` instead of message vars. The **Send to CLI Session** action ([cli_send_to_session](/src/main/services/automation/legacy/automation-action-executor.ts)) safely skips (rather than throws) when the target session is archived, deleted, paused, or missing — so a stale rule won't break the chain. Full design + preset catalog: [feature-inventory-channels-automations.md](/.claude/memory/feature-inventory-channels-automations.md). Landing page marketing: _"Automations triage your messages. And when one account hits its cap, the next one picks up instantly."_

**Gmail-scoped rules** are fed by a background poll of your connected inbox every 2 minutes — [/src/main/services/email/gmail-automation-poller.ts](/src/main/services/email/gmail-automation-poller.ts) — that reads the newest 20 inbox threads from the last **6 days**. Mail that arrived while Omniscio was closed therefore still triggers your rules when it reopens, but nothing older than 6 days does. Each email is handled at most once: a claim remembers it for 7 days ([/src/main/db/queries-automation-dedup.ts](/src/main/db/queries-automation-dedup.ts)), and the search deliberately stops a day short of that, so an old email can never trigger the same rule twice.

**AI edit sessions** launched from the Automations virtual project's "AI sessions" category get a pre-seeded first prompt so the Claude Code agent has full domain context before you type a word. The seeding happens in [/src/main/services/session/session-create.ts](/src/main/services/session/session-create.ts) — when `project.folderPath === AUTOMATIONS_PROJECT_ID`, it calls [/src/main/services/virtual-project-seed.ts](/src/main/services/virtual-project-seed.ts) to assemble a bundle (this library page + your current rule inventory as JSON + a REST cheatsheet for `/automation/*` + auth info), mints an in-app session token via [/src/main/services/in-app-session-token.ts](/src/main/services/in-app-session-token.ts), formats the combined text as the first CLI prompt, and persists it as a `source: 'system'` row in `conversation_messages` so the chat UI renders it as injected context rather than operator input. Rules the AI creates via `/automation/*` using the in-app token are recognised by [/src/main/services/cli/cli-server.ts](/src/main/services/cli/cli-server.ts) `getAuthContext()` as the `in-app-session` class and skip the normal approval inbox — the trust boundary is "this session was launched by the user from inside Omniscio".

## Related

This page is the companion to [Automations & Auto-replies (auto-respond to messages)](automations-and-auto-replies.md), which covers the feature itself: what the two systems are, where to find them, and how to configure a rule. [quick-replies.md](quick-replies.md) is the paired virtual project for the snippet library the reply action cites, [automation-credentials.md](automation-credentials.md) holds the secrets the v2 actions load, [dev-pipeline-panel.md](dev-pipeline-panel.md) can seed a starter set of workflow auto-replies, and [waiting-detector.md](waiting-detector.md) owns the check-in tags that render instead of a badge.
