Files
openhuman/docs/plans/tinyflows-integration/triggers.md
T

141 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Trigger taxonomy & the cron → workflows unification
> Companion to [README.md](README.md) (the tinyflows completion POA). This doc answers two questions: **what kinds of triggers can this platform offer**, and **how do we migrate cron so that "everything automated is a workflow"**.
## 0. Thesis
Almost every automated behavior in OpenHuman is already shaped like *event → conditions → actions*: cron jobs, Composio app events, channel-message reactions, subconscious escalations, meeting follow-ups, notification triage. Today each has bespoke plumbing. tinyflows gives us one uniform substrate (a tinyagents graph per run, durable, observable, approval-gated), and the event bus (`src/core/event_bus/events.rs`, ~130 `DomainEvent` variants) is already the spine every one of those signals travels on.
**End state**: *Workflows are the single user-facing automation concept.* A trigger is just a subscription that seeds a flow run; the trigger catalog is a curated projection of the event bus plus time and manual entry points.
## 1. What the engine already models
`tinyflows::model::TriggerKind` (declarative — the host fires them): `manual`, `schedule`, `webhook`, `app_event`, `form`, `execute_by_workflow`, `chat_message`, `evaluation`, `system`.
Host dispatch status today (`flows/bus.rs`):
| Kind | Status |
| --- | --- |
| `manual` | ✅ `flows_run` RPC / UI Run button |
| `schedule` | ✅ cron `JobType::Flow``FlowScheduleTick` |
| `app_event` | ✅ `ComposioTriggerReceived` matching (toolkit + trigger_slug) |
| `webhook` | ⏸ deferred (Composio is the webhook story — README Phase 1a) |
| `chat_message`, `form`, `execute_by_workflow`, `evaluation`, `system` | ❌ no dispatcher |
The gap is not engine capability — it's host dispatchers and a catalog. Crucially, `system` is a free config surface: we can build **one generic event-trigger dispatcher** instead of nine bespoke ones.
## 2. Proposed trigger catalog
Organized by source. "Backing event" = the existing `DomainEvent` variant(s) that would feed the dispatcher.
### 2.1 Time (`schedule`)
Already live via cron. Expose the full richness cron already has (`CronJob` in `cron/types.rs`):
- **Cron expression** (today's path) and **interval** ("every 15 min") sugar.
- **One-shot** ("run once at T") — cron's `delete_after_run` already models this; surface it in trigger config.
- **Windowed schedules** (weekdays only, quiet hours) — respects `scheduler_gate`.
### 2.2 Manual & parameterized (`manual`, `form`)
- **Manual**: Run button / RPC / agent-initiated (`flows_run` with input).
- **Form**: a manual run with a typed input schema. Trigger config declares fields; the UI renders a form; submission becomes the trigger payload. This is n8n's Form trigger and gives every workflow a shareable "mini-app" entry point. Cheap to ship: validation + a generated form in the canvas/run dialog.
### 2.3 External apps (`app_event`, `webhook`)
- **Composio triggers** (live): Gmail new message, Slack event, GitHub push, calendar event, … the platform delivers; we match toolkit/slug. Work remaining is subscription lifecycle + catalog surfacing (README Phase 1a).
- **Raw webhook** (deferred): non-Composio HTTP callers via `webhooks::ops` tunnels. Backlog until demanded.
### 2.4 Conversation (`chat_message`)
Backing events: `ChannelInboundMessage` / `ChannelMessageReceived` / `ChannelReactionReceived`.
- **Message received** with filters: provider/channel id, sender, regex or keyword, mention-of-me, DM vs group.
- **Reaction added** (e.g. ✅ on a message → file it as a task).
- Guardrail: these runs consume untrusted text — `prompt_injection` screening before any `agent` node, and a per-flow debounce/concurrency cap (the `app_event` dispatcher's guard generalizes).
### 2.5 Agent & task lifecycle (`system`)
Backing events: `AgentTurnCompleted`, `SubagentCompleted`/`SubagentFailed`, `ApprovalDecided`, `TaskSourceTaskIngested`, `TaskRunReclaimed`, `ThreadGoalUpdated`.
Examples: "when any subagent fails, post a summary to my ops channel"; "when a task is ingested from a task source, enrich and label it"; "when an approval is denied, notify the requester thread".
### 2.6 Memory & knowledge (`system`)
Backing events: `MemoryStored`, `MemoryIngestionCompleted`, `MemoryDiffComputed`, `DocumentCanonicalized`, `TreeSummarizerHourCompleted`.
Examples: "when a document is canonicalized, generate an abstract and store it"; "when the hourly summary lands, check for action items".
### 2.7 Meetings & voice (`system`)
Backing events: `MeetingSessionCreated`, `MeetingSummaryGenerated`, `BackendMeetTranscript`, `PttTranscriptCommitted`, `MeetingAutoJoinTriggered`.
Examples: "when a meeting summary is generated, extract action items → create tasks → email attendees" — the flagship demo workflow.
### 2.8 Notifications & devices (`system`)
Backing events: `NotificationIngested`/`NotificationTriaged`, `DevicePaired`, `DevicePeerOnline`/`Offline`.
Examples: triage automations ("when a notification from app X is ingested, decide urgent/ignore"), presence automations ("when my phone comes online, sync …").
### 2.9 Platform & health (`system`)
Backing events: `SystemStartup`, `HealthChanged`, `HealthRestarted`, `AutonomyConfigChanged`, `McpServerDisconnected`, `ProviderApiKeyRejected`, `EmbeddingModelUnhealthy`.
Examples: self-healing/maintenance workflows ("on health degradation, run doctor and report"). These should default to `require_approval` off but notification-heavy.
### 2.10 Subconscious escalations (`system`)
Backing events: `SubconsciousTriggerProcessed`, `TriggerEvaluated`, `TriggerEscalated`.
The subconscious domain is itself a mini automation engine (evaluate → escalate). Long-term convergence candidate: an escalation's *action* becomes "run flow X", making workflows the actuator layer for subconscious signals rather than a parallel system.
### 2.11 Workflow-to-workflow (`execute_by_workflow`, `evaluation`)
- `execute_by_workflow`: fired when another flow's `sub_workflow`/by-id call targets this flow (README Phase 4b) — enables composition and shared "library" flows.
- `evaluation`: reserved for eval-harness runs of a flow (regression-test a workflow against recorded inputs); pairs with `dry_run_workflow` (README Phase 5b).
## 3. Design: one generic event dispatcher, not N bespoke ones
Adding a hand-written `handle_*` per event (like `handle_app_event`) doesn't scale to §2.52.10. Proposal:
1. **Trigger catalog registry** (new, `flows/trigger_catalog.rs`): a curated allowlist of `DomainEvent` variants exposed as triggers — `{ key: "meeting.summary_generated", event: <variant match>, payload_schema, description, risk_class }`. Only cataloged events are subscribable; the raw bus is never exposed wholesale.
2. **Generic dispatcher** in `FlowTriggerSubscriber`: one match arm per cataloged domain that projects the event into a stable JSON payload, then matches enabled flows whose trigger is `kind: system, config: { event: "<key>", filter: "=<jq expr>" }`. The existing jq engine (`tinyflows::expr`) evaluates the optional filter against the payload — no new expression language.
3. **RPC** `flows_list_trigger_catalog()` → feeds the Phase 3 trigger-config UI and the Phase 5 builder agent (so prompts like "when a meeting ends…" ground to a real catalog key).
4. **Guardrails** (non-negotiable, enforced in the dispatcher):
- **Loop prevention**: flow runs emit events too; runs triggered by a flow-originated event carry a provenance chain with a max depth (reuse tinyagents `root_run_id` lineage); a flow can never trigger itself.
- **Debounce/rate limits**: per-flow trigger token bucket + the existing per-flow concurrency guard.
- **Payload hygiene**: catalog projection strips PII-ish fields by default; `prompt_injection` screening before agent nodes.
- **Risk classes**: catalog entries tagged (e.g. conversation events = untrusted-input class → forces screening; health events = internal class); validation surfaces the class in the UI.
## 4. Cron → workflows migration
Today `cron` runs three job types (`cron/types.rs::JobType`): `Shell`, `Agent`, and `Flow` (already just a tick-publisher for flows). The inversion: **cron stops being a product surface and becomes the schedule service for workflows.**
**M1 — Model mapping.** Every legacy job is a one-node flow:
- `JobType::Shell { command }` → flow: `trigger(schedule)``code` node (or a dedicated `shell` tool_call, subject to the same `classify_command` gating cron uses today).
- `JobType::Agent { prompt, agent_id, model, session_target }` → flow: `trigger(schedule)``agent` node (prompt/model in config; `agent_id` maps to the Phase 5+ agent-definition reference once engine sub-ports land — until then, definition prompt inlined).
- `DeliveryConfig` / `session_target` (post result to main thread vs isolated) → flow-level `delivery` setting or an explicit terminal "notify/post" node. Prefer the explicit node: it's visible on the canvas, which is the whole point.
- `delete_after_run` → one-shot schedule config (§2.1).
**M2 — Dual-write bridge.** `cron_add`/`cron_update` agent tools and RPCs keep working but create flows under the hood (a `legacy_cron` tag preserves round-tripping for `cron_list`). New UI creation always goes through flows.
**M3 — Backfill migration.** One-time migration (per-user, on upgrade): convert existing enabled shell/agent jobs into flows, preserving ids in metadata, next_run continuity, and run history linkage (`CronRun` rows stay readable; new runs are `FlowRun`s). Feature-flag + rollback window before deleting the legacy paths.
**M4 — Surface consolidation.** Settings → Automations lists flows only; cron internals (`scheduler.rs`, jobs table) remain as the tick engine for `JobType::Flow`, which becomes the *only* job type. The `cron_*` tool family shrinks to a thin alias the agent can still call, documented as "creates a scheduled workflow".
**What cron keeps**: the single tokio scheduler loop, persistence of next-fire times, catch-up semantics, `scheduler_gate`. That's infrastructure — workflows are the product on top of it.
**Sequencing**: M1/M2 fit inside README Phase 1 (they're mostly `flows`-side); M3/M4 after the canvas + run inspector are solid (users must be able to *see* what their migrated jobs became), i.e. post-Phase 3.
## 5. Rollout order for new trigger kinds
1. `form` + one-shot/interval schedule sugar (cheap, immediately useful).
2. Trigger catalog + generic `system` dispatcher, seeded with a small vetted set: `meeting.summary_generated`, `notification.triaged`, `subagent.failed`, `task.ingested`, `document.canonicalized`.
3. `chat_message` (needs the untrusted-input guardrails first).
4. Cron migration M1M2, then M3M4.
5. `execute_by_workflow` (with Phase 4b sub-workflow-by-id), `evaluation` last.
Each new dispatcher lands with: E2E test in `tests/json_rpc_e2e.rs`, catalog entry with payload schema, validation warning removal (the kind moves from "declared but never fires" to live), and a template (README Phase 4c) demonstrating it.