# Automation Ontology — the entity canon of every Fractera automation

> **The fundamental starting point.** Every automation in this workspace — built by a human or
> generated by a model — is composed of EXACTLY the thirteen entities below. Before you author,
> extend or review an automation (a project of the Projects layer), read this glossary: it defines
> what exists, how each entity works, and what task it solves. An automation described outside this
> ontology is a defect, not a variation. The decomposition engine
> (`orchestrate-project-by-steps`, schema v2 / engine ≥0.8) VALIDATES these entities — a graph
> that breaks the gates is refused, so the canon is enforced, not advisory.
>
> Entities 1–12 describe ONE automation in isolation; entity 13 (**Subject**) and the **inter-automation
> pub/sub** mechanism (§D) describe how SEVERAL automations cooperate — one automation hands work to
> another by publishing a named event, and a shared long-lived Subject carries state + history across them.

This is the industry-converged **ECA model** (Event–Condition–Action — the backbone of Zapier,
IFTTT, n8n, Home Assistant, Temporal), specialized for Fractera's voice-command automations.

**The one iron rule (contract R6):** the automation's schema (its diagram) is the ONLY truth of
execution. A new outcome = a new **Action** in the graph + an engine re-run — never a "shadow
step" hand-added in code. What is not on the diagram does not exist.

---

## A. Rule definition — what the schema declares

### 1. Automation (the Rule)
- **What it is:** the container: "when X happens, under condition Y, do Z". One project of the
  Projects layer (e.g. `personal/telegram-notes`) = one automation.
- **How it works:** materialized by the decomposition engine from a typed graph into a project
  page (diagram + settings + records table), a durable workflow and a step queue.
- **What it solves:** turns a repeated manual behavior into a self-running, observable unit with
  one page of truth.
- **Schema:** the graph root `{ category, slug, project{purpose,efficiency,reuse,result},
  actions[], state[], nodes[], record{table,fields[]} }` (the optional `record` block declares the
  universal records table's columns — see entity 12).

### 2. Trigger
- **What it is:** the event source that starts a run: an incoming message, a schedule (cron), a
  manual run, a webhook, or a **published event from ANOTHER automation** (`kind: "event"` — the
  inter-automation entry point, §D).
- **How it works:** a node of `kind: "trigger"` (or `kind: "event"` for a subscriber); it is not a
  workflow step — it IS the run route / cron declaration / run panel. An `event` trigger declares
  which named events it `subscribes` to; the co-located `events.json` maps those names to this
  automation's `/run` route, and the substrate dispatcher starts a run when a subscribed event fires.
- **What it solves:** defines WHEN the automation wakes up — including waking up because a sibling
  automation handed work over.
- **Schema:** node `{ kind: "trigger" }` (+ `cron.json` for schedules) · node
  `{ kind: "event", subscribes: [eventName] }` (+ `events.json` for subscriptions).

### 3. Hook
- **What it is:** the user's spoken vocabulary: a phrase bound to one Action ("remember this" →
  save). Fractera's voice-command specialty — the human-facing trigger filter.
- **How it works:** phrases live in the GLOBAL `project_hooks` table (app-wide unique, normalized
  lowercase, no punctuation); the Router matches incoming text against them and routes the message
  to the owning project + action.
- **What it solves:** lets a person drive automations by saying things, in any wording the phrase
  captures — one phrase maps to exactly one action app-wide.
- **Schema:** declared per action: `actions[].hooks: [{ phrase, lang }]` — the registry seeds the
  project's default hooks; users add their own in Settings.

### 4. Condition
- **What it is:** a declared guard: "run this only if …" (a date was parsed; the chat is allowed;
  inside working hours).
- **How it works:** DECLARATIVE in the schema (text + optional machine hint). The engine requires
  it to be named, the diagram shows it, the coder-handoff carries it verbatim, and the records
  table shows which condition path a record took. The EXECUTION of the guard lives in the workflow
  step code (R6 isomorphism) — the schema documents the branch, the code implements it.
- **What it solves:** makes branching honest and visible; without declared conditions a diagram
  lies about when things happen.
- **Schema:** `actions[].condition?: string` and/or `nodes[].condition?: string`.

### 5. Action
- **What it is:** a named outcome of the automation — an EVENT consisting of a sequence of steps
  (save a note; deliver a date reminder; answer a memory search). The central scalable entity:
  configuring an automation = configuring Actions bound to Hooks.
- **How it works:** a first-class registry entry with an id, title, description, color, its hooks,
  its condition and its delivery channel. Every work node declares WHICH actions flow through it;
  the diagram renders one visually distinct branch per action (single-action nodes carry the
  action's color; shared trunk nodes stay neutral).
- **What it solves:** gives models and humans a typed unit to add/modify; the records table, the
  settings dropdown and the diagram all speak in Actions — never in abstract verbs.
- **Inter-automation (optional `emits`):** an Action may PUBLISH a named event when it runs —
  `emits: { event, subjectTransition? }` (§D). Publishing is fire-and-forget pub/sub: the Action does
  NOT name a target automation; any automation that `subscribes` to that event name wakes up. When the
  Action acts on a Subject, `subjectTransition` moves that Subject's status (e.g. `replied`).
- **Schema:** `actions: [{ id, title, description, color?, hooks[], condition?, channel, emits? }]`.

### 6. Router
- **What it is:** the classifier step: takes an incoming event and decides which Action it belongs
  to (or ignore).
- **How it works:** a node of `kind: "router"` (e.g. detect-hook: first ~20 words → match against
  registered hooks → `{actionId, hookPhrase, payload}` or ignore).
- **What it solves:** the single entry point of the command mode — one place where language turns
  into a typed action id.
- **Schema:** node `{ kind: "router" }`; required whenever the automation declares hooks.

### 7. Step
- **What it is:** an atomic operation: call an API, transform data, write a row.
- **How it works:** a node of `kind: "step"` (work) or `"transform"` (pure reshaping), implemented
  as a `"use step"` function under its `// node:<id>` marker in the durable workflow.
- **What it solves:** the unit of implementation and retry; a coder builds exactly one step per
  materialized sub-step file.
- **Schema:** `nodes[]: { id, title, kind: trigger|router|step|transform, actions: string[]|"all",
  condition?, errorPolicy?, description, task, tools[], envKeys[], io{in,out}, todo[],
  dependsOn[] }`. (`kind:"action"` from schema v1 is accepted as an alias of `step`.)

## B. Resources — what the automation needs

### 8. Integration
- **What it is:** an external service + its credentials (Telegram Bot API, OpenAI, LightRAG).
- **How it works:** declared as `{ name, envKeys[] }`; keys are persisted via the env setter /
  missing-keys modal (never hardcoded); steps reference keys via `envKeys`.
- **What it solves:** makes every external dependency and secret visible, checkable ("check key")
  and configurable in three places (entry modal, settings, node panel).
- **Schema:** project `integrations[]` (README `fractera:meta`) + per-node `envKeys[]`.

### 9. Channel
- **What it is:** where an Action's output is delivered: the bot chat, (later) email, a page.
- **How it works:** a declared label on the action; the final reply/delivery step honors it.
- **What it solves:** separates WHAT an action produces from WHERE it lands — swapping the
  destination never rewrites the action.
- **Schema:** `actions[].channel: string` (e.g. `"telegram-bot-chat"`).

### 10. State
- **What it is:** persistent data the automation keeps BETWEEN runs: the `last_update_id` poll
  cursor, the vector memory corpus.
- **How it works:** declared in a registry so no model reinvents storage; implemented as SCHEMA
  tables / memory ingests.
- **What it solves:** dedup, resumability, and semantic recall survive restarts and redeploys.
- **Schema:** `state: [{ id, storage, purpose }]`; steps reference state ids in their `task`.

## C. Runtime — what execution produces

### 11. Run
- **What it is:** one execution instance (runId, status, journal).
- **How it works:** the durable workflow (WDK, world-local) journals into `project_cron_runs`;
  the run panel starts manual runs; cron starts scheduled ones.
- **What it solves:** observability — every tick is accountable.

### 12. Record
- **What it is:** a durable result row: the note saved, the reminder scheduled, the search
  answered — plus its memory document.
- **How it works:** written by steps into the automation's own table; shown in the ONE universal
  records table with the columns **Action · Hook (the phrase that fired) · Summary · Condition ·
  Due · Created · Full text** — the full trace from what the user said to what ran.
- **What it solves:** the user sees and searches everything the automation ever did, by meaning
  (server-side search + semantic recall via memory).
- **Lifecycle (create → trace → DELETE):** a Record is created by steps and is **deletable by
  the owner**. The universal records table carries a **Delete** button in its LAST column;
  clicking it opens a **confirmation dialog**, and on confirm the automation deletes BOTH the DB
  row AND its vector-memory document (best-effort). Two-store deletion requires the Record to
  REMEMBER its memory document id: the ingest step captures the id returned by the memory store
  and stores it on the Record (a `memory_doc_id` column). Vector deletion uses the memory
  service's per-document delete (LightRAG `DELETE /documents/delete_document`); if the memory id
  is unknown or the call fails, the DB row is still removed and the orphaned vector document is
  swept later — the owner is never blocked. A `DELETE /api/projects/<cat>/<slug>/notes/[id]`
  endpoint (role-gated) performs both deletions. This is the standard, reusable Record contract
  every automation inherits.
- **Columns are CONFIG-DRIVEN (the table is universal, never hand-coded):** the automation DECLARES
  which columns the records table shows — it does NOT hand-write a bespoke table component. The graph
  root carries an optional `record` block; the engine GENERATES `_data/columns.ts` from it (marker
  `// fractera:columns`, a re-run rewrites it — exactly like `_data/actions.ts`). The frozen primitive
  ships ONE universal `RecordsTable` that renders whatever `columns.ts` declares, through a CLOSED set
  of typed renderers; adding a column = extend the GRAPH and re-run, never edit a component.
  - **Schema:** `record: { table, fields: [{ id, header, type, source, defaultVisible?, attr?, options? }] }`.
    - `attr?` — the ontology entity this column surfaces (`action|hook|condition|channel|state|…`); the
      column picker labels the checkbox by it (with a tooltip) from the shared attribute vocabulary.
    - `table` — the DB table the rows come from (declared in `state[]`; a column can only show data the
      automation actually persists — a checkbox reveals an existing field, it never invents one).
    - `type ∈ { badge | text | longtext | date | link | actions }` — the closed renderer set (`badge`
      tints via `options.colorFrom`; `longtext` expands on click; `date` may `emphasizeIfFuture`;
      `actions` renders row actions `detail`/`delete`).
    - `source` — the `table` column feeding the cell. `defaultVisible` — shown before the user toggles.
  - **Two layers, never conflated:** the AUTOMATION declares which columns EXIST + are default-visible
    (config, per-automation — the "3 columns vs 6" scaling); the USER toggles VISIBILITY among them via
    a checkbox picker (personal, `localStorage`, horizontal scroll when many). The picker labels each
    column by its ontology entity from the shared attribute vocabulary.

## D. Cross-automation — how several automations cooperate

Entities 1–12 describe one automation. Real work spans several: an outreach automation hands a lead to a
dialog automation; a support automation escalates a ticket. Two constructs carry this — a **Subject** (the
shared long-lived object) and **pub/sub events** (the typed handoff). The rule: **cooperation is an EVENT +
a status change on a shared Subject, NEVER a row copied between per-state tables** (copying duplicates
identity and forces cross-table joins for history).

### 13. Subject
- **What it is:** a long-lived object that MULTIPLE automations act on over time — a blogger, a lead, a
  customer, a ticket. Distinct from a **Record** (entity 12): a Record is the result of ONE run of ONE
  automation; a Subject is the object itself, outliving any single run or automation.
- **How it works:** one shared `subjects` table (app-wide, stable `id`): `{ id, kind, status,
  owner_automation, attributes(JSON), created_at, updated_at }`. `status` is a state machine declared by
  the graph's `subject` block (`statuses[]` + allowed `transitions[]`); an Action moves it via
  `emits.subjectTransition`. Every touch appends to an **append-only `subject_events` log** keyed by
  subject id — the full history is one query on one id, no table hopping. `owner_automation` names which
  automation currently drives the Subject (a "frozen" subject is simply one no automation is selecting).
- **What it solves:** a single identity + a single timeline across every automation that ever touched it —
  the thing the user calls "the blogger", not a scatter of per-state rows.
- **Schema:** graph root `subject: { kind, statuses: [id...], transitions: [{ from, to }...] }`; the shared
  `subjects` + `subject_events` tables are substrate (declared once, like `project_cron_runs`).

### Inter-automation pub/sub (the handoff mechanism)
- **Publish:** an Action's `emits: { event, subjectTransition? }` writes a row to the substrate
  `automation_events` queue (event name + subject id) and, when given, applies the Subject transition. The
  publisher does NOT know who listens.
- **Subscribe:** an automation declares a `kind: "event"` Trigger with `subscribes: [eventName]`; its
  co-located `events.json` maps the name to this automation's `/run` route.
- **Dispatch:** the substrate dispatcher (the fractera-cron runner, extended) drains `automation_events`,
  finds every subscriber for each event, and `POST`s its `/run` with `{ subjectId, event }` — the subscriber
  reads the Subject's CURRENT state (the payload carries only the id, never a stale copy). Zero new
  infrastructure: it mirrors how `cron.json` schedules already fire `/run`.
- **Why pub/sub (not a direct A→B target):** loose coupling — A keeps working if B is renamed or removed,
  and several automations may subscribe to the same event. The event NAME is the contract.

## Standardized attributes (fields, not entities)

- **errorPolicy** — per step/action: `retry-next-tick` (a failure is retried by the next run) ·
  `soft-degrade` (a fallback result, the run continues) · `fail-run` (abort). Default:
  `soft-degrade` for enrichment steps, `retry-next-tick` for delivery.
- **Permissions** — who may view/configure/trigger; carried by the project page roles
  (architect + manager) — do not re-declare per node.

---

## Validation gates (engine ≥0.8 — what makes this a canon, not a wish)

1. Every declared Action has ≥1 node dedicated to it (`node.actions` includes it).
2. Every node's `actions` ⊆ declared action ids (or `"all"` for trunk nodes).
3. A `router` node exists whenever any action declares hooks.
4. An action without hooks → warning (unreachable by voice; must be triggered another way).
5. An action or node naming a `condition` must state it (non-empty text).
6. Every `state` id referenced by a step's task exists in the registry.
7. Isomorphism (R6): diagram nodes ↔ `// node:<id>` workflow markers; kept workflow files are
   validated, never overwritten.
8. Records columns (when a `record` block is present): `record.table` is an id-safe identifier (the
   SQLite table the rows come from — typically the table a `state` entry persists to); there is ≥1
   field; every `field.type` ∈ the closed set `{badge|text|longtext|date|link|actions}`; every
   `field.source` is an id-safe column identifier; at least one field is `defaultVisible`. A broken
   type, a non-identifier table/source, or an empty field set → the engine refuses the graph.
9. Inter-automation, event trigger (§D): a node of `kind: "event"` must declare a non-empty
   `subscribes` list of event names — a subscriber with nothing to subscribe to is refused.
10. Inter-automation, emit (§D): an Action's `emits.event` (when present) is a non-empty event name;
    if `emits.subjectTransition` is given, a `subject` block must exist and the transition's target
    status ∈ the declared `subject.statuses`. A publish to a status outside the machine → refused.
11. Subject (when a `subject` block is present): `kind` is non-empty; `statuses[]` has ≥1 entry;
    every `transitions[].from`/`.to` ∈ `statuses`. A transition referencing an undeclared status →
    refused (the state machine must be closed).

## Worked example — telegram-notes (3 actions)

- Actions: `save` "Save a note" (blue, hooks: "remember this…", channel bot-chat) · `remind`
  "Date reminder" (amber, condition: "a date/time is parsed from the message; otherwise ask
  when", channel bot-chat) · `recall` "Memory search" (green, hooks: "what did I save about…",
  channel bot-chat).
- Trunk (actions: all): trigger → fetch-updates → detect-hook (router) → … → reply.
- Branches: summarize/persist/ingest serve `save`+`remind`; search-memory serves `recall`;
  deliver-due-reminders serves `remind` (off the cron trigger).
- State: `telegram-cursor` (SQLite, dedup of getUpdates), `notes-memory` (LightRAG, semantic
  recall). Records: one row per save / remind / recall with hook_phrase + condition outcome.
- Record columns (`record.table: telegram_notes`): `Action` (badge, colorFrom the action) · `Hook`
  (text) · `Summary` (longtext, expands) · `Condition` (text) · `Reminder` (date, emphasizeIfFuture) ·
  `Created` (date) · `Full text` (actions detail) · Delete (actions delete). The engine emits these
  into `_data/columns.ts`; the universal `RecordsTable` renders them — no bespoke table component.
