bot0-trigger-system-architecture.md

bot0 Trigger System Architecture

The trigger system enables bot0 to react to real-world events — new emails, competitor website changes, social media mentions, price drops, deploy failures — and execute automated responses. Triggers are the event-driven counterpart to cron schedules: where schedules run on time, triggers run on change.


Core Concepts

What is a Trigger?

A trigger is a condition-action pair: when X happens, do Y. The "when" is the detection mechanism (how we know something changed), and the "do" is the prompt (what the agent should do about it).

┌─────────────────────────────────────────────────────────────────────────────┐
│                          TRIGGER ANATOMY                                     │
│                                                                              │
│   ┌─────────────────────┐     ┌──────────────────────┐                     │
│   │     DETECTOR         │     │     RESPONDER         │                     │
│   │                      │     │                       │                     │
│   │  "When something     │────▶│  "Do this when it     │                     │
│   │   changes"           │     │   fires"              │                     │
│   │                      │     │                       │                     │
│   │  • Composio webhook  │     │  • Agent prompt       │                     │
│   │  • API poll + diff   │     │  • Target daemon      │                     │
│   │  • DOM observation   │     │  • Tool access         │                     │
│   │  • Custom script     │     │  • Permission mode    │                     │
│   └─────────────────────┘     └──────────────────────┘                     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Every trigger — whether it's a native Composio webhook or a custom DOM scraper — produces the same output: an event that starts a new agent session on the target daemon.

Trigger Types — Priority Hierarchy

When the user wants to monitor something, the system chooses the cheapest, most reliable detection method:

┌─────────────────────────────────────────────────────────────────────────────┐
│                  TRIGGER TYPE SELECTION (priority order)                     │
│                                                                              │
│   ┌─────────────────────────────────────────────────────────────────┐       │
│   │ 1. COMPOSIO NATIVE TRIGGER                                      │       │
│   │    Webhook-based, real-time, zero polling cost                  │       │
│   │    Example: Gmail new email, GitHub PR opened, Slack mention    │       │
│   │    Detection: Composio pushes webhook → Bytespace → daemon     │       │
│   │    Cost: $0 per fire                                            │       │
│   │    Latency: ~seconds                                            │       │
│   │    Setup: Single API call (bytespace_trigger create)            │       │
│   └─────────────────────────────────────────────────────────────────┘       │
│        │                                                                     │
│        │ Not supported?                                                      │
│        ▼                                                                     │
│   ┌─────────────────────────────────────────────────────────────────┐       │
│   │ 2. CRON + API POLLING                                           │       │
│   │    Scheduled API calls (Composio or direct), diff stored state  │       │
│   │    Example: Instagram new comments, Notion page changes         │       │
│   │    Detection: Cron runs → API fetch → diff → fire if changed   │       │
│   │    Cost: ~$0 per poll (API only, no LLM)                       │       │
│   │    Latency: cron interval (e.g., every 5 minutes)              │       │
│   │    Setup: Agent writes detection script + creates schedule      │       │
│   └─────────────────────────────────────────────────────────────────┘       │
│        │                                                                     │
│        │ No API available?                                                   │
│        ▼                                                                     │
│   ┌─────────────────────────────────────────────────────────────────┐       │
│   │ 3. CRON + DOM0 (browser automation)                             │       │
│   │    Headless browser checks via Chrome DevTools Protocol         │       │
│   │    Example: Competitor website updates, price monitoring        │       │
│   │    Detection: Cron runs → dom0 snapshot → diff → fire          │       │
│   │    Cost: ~$0 per poll (no LLM, deterministic DOM)              │       │
│   │    Latency: cron interval                                       │       │
│   │    Setup: Agent writes detection script + creates schedule      │       │
│   └─────────────────────────────────────────────────────────────────┘       │
│        │                                                                     │
│        │ No stable DOM structure?                                            │
│        ▼                                                                     │
│   ┌─────────────────────────────────────────────────────────────────┐       │
│   │ 4. CRON + CMD0 / CUSTOM SCRIPT                                  │       │
│   │    Visual grounding or arbitrary code for detection             │       │
│   │    Example: Desktop app state, visual dashboards, RSS feeds    │       │
│   │    Detection: Cron runs → script/cmd0 → diff → fire            │       │
│   │    Cost: ~$0.01+ per poll (cmd0 uses grounding model)          │       │
│   │    Latency: cron interval                                       │       │
│   │    Setup: Agent writes detection script + creates schedule      │       │
│   └─────────────────────────────────────────────────────────────────┘       │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Type 1 (Composio native) is always preferred — it's real-time, zero-cost, and requires no custom code.

Types 2-4 (custom) share the same pattern: a cron schedule runs a detection script that diffs current state against a checkpoint. The agent builds these using existing tools — no special framework needed.

Design Principles

  1. Native first — Always check if Composio has a native trigger before building a custom one.

  2. Deterministic detection, always — Detection scripts never use LLM. Detection is a programmatic diff: "is the current state different from the stored state?" The LLM only runs in the response phase, after the trigger has already fired. This keeps polling cost at $0 and is enforced structurally (see Enforcing No-LLM Detection).

  3. One table — All triggers (native + custom) live in ctx0_triggers. One source of truth.

  4. State on the row — Each trigger's checkpoint state is a JSONB column on the trigger record itself. No separate state table, no local files. State persists across daemon restarts and daemon migrations.

  5. Powered by schedules — Custom triggers are cron schedules with a detection script. The existing schedule infrastructure handles timing, daemon targeting, and execution. Triggers add change detection on top.

  6. No special builder mode — The agent builds custom triggers using existing tools: write a script, test it, create a schedule. No trigger_builder agent mode, no TRIGGER.md manifest format. The agent's normal coding abilities are sufficient.

  7. Test before deploy — Before a custom trigger goes live, the agent dry-runs the detection script against real data and shows what would fire. The user confirms before deployment.


Single Trigger Registry

The Problem with Two Tables

The current implementation has ctx0_composio_triggers for native Composio triggers. Adding a second table for custom triggers creates confusion: which table do you query to "list all triggers"? Which is the source of truth for a trigger that started as custom but was later replaced with a native one?

The Solution: One Unified Table

All triggers — native Composio webhooks, cron-based API pollers, dom0 scrapers, custom scripts — are rows in ctx0_triggers. The type column discriminates.

sql
CREATE TABLE ctx0_triggers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id), -- Identity name TEXT NOT NULL, -- lowercase-hyphenated, unique per user description TEXT NOT NULL, -- Type discrimination type TEXT NOT NULL, -- 'composio_native' | 'cron_api' | 'cron_dom0' | 'cron_script' -- Composio native fields (only when type = 'composio_native') composio_trigger_id TEXT, -- External Composio trigger instance ID composio_slug TEXT, -- e.g., 'GMAIL_NEW_GMAIL_MESSAGE' composio_config JSONB, -- Composio trigger configuration -- Custom trigger fields (only when type starts with 'cron_') detection_script TEXT, -- The detection script content (synced across daemons) cron_expression TEXT, -- e.g., '*/5 * * * *' cron_timezone TEXT DEFAULT 'UTC', schedule_id UUID REFERENCES ctx0_schedules(id), -- Linked cron schedule -- State checkpoint (custom triggers only) -- Updated every poll. Contains last_seen_ids, hashes, timestamps, etc. -- Reset to '{}' to reprocess everything. state JSONB NOT NULL DEFAULT '{}', -- Shared fields (all trigger types) toolkit TEXT, -- Which service (gmail, instagram, etc.) response_prompt TEXT NOT NULL, -- What the agent does when trigger fires response_model TEXT, -- Model override (null = daemon default) permission_mode TEXT DEFAULT 'auto', -- auto | ask | dangerously_auto -- Targeting target_daemon_id UUID REFERENCES ctx0_daemons(id), target_device_id TEXT, -- Stats status TEXT DEFAULT 'active', -- active | paused | error_paused | disabled fire_count INTEGER DEFAULT 0, poll_count INTEGER DEFAULT 0, -- Custom triggers only last_fired_at TIMESTAMPTZ, last_polled_at TIMESTAMPTZ, -- Custom triggers only -- Error tracking (custom triggers only) last_error TEXT, -- Last detection error message (stderr or timeout) last_error_at TIMESTAMPTZ, -- When the last error occurred consecutive_failures INTEGER DEFAULT 0, -- Resets to 0 on successful poll error_log JSONB DEFAULT '[]', -- Capped array of last 20 errors (see Error Log below) -- Improvement notes (same pattern as skills) improvement_notes JSONB DEFAULT '[]', -- Provenance source_session_id UUID, source_daemon_id UUID, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), synced_at TIMESTAMPTZ, -- Last sync with Composio (native only) UNIQUE(user_id, name) ); CREATE INDEX idx_triggers_user ON ctx0_triggers(user_id); CREATE INDEX idx_triggers_user_status ON ctx0_triggers(user_id, status); CREATE INDEX idx_triggers_daemon ON ctx0_triggers(target_daemon_id);

Migration from ctx0_composio_triggers: Data migrates to ctx0_triggers with type = 'composio_native'. The old table is dropped after migration. All existing code (poller, executor, webhook handler) updates to read from the new table.

Data Model

┌─────────────────────────────────────────────────────────────────────────────┐
│                    TRIGGER DATA MODEL                                       │
│                                                                              │
│   ctx0_triggers (SINGLE SOURCE OF TRUTH)                                   │
│   ┌────────────────────────────────────────────────────────────────┐       │
│   │ id, user_id, name, type                                        │       │
│   │                                                                  │       │
│   │ ┌── composio_native ──┐  ┌── cron_api / cron_dom0 / etc ──┐  │       │
│   │ │ composio_trigger_id  │  │ detection_script                │  │       │
│   │ │ composio_slug        │  │ cron_expression                 │  │       │
│   │ │ composio_config      │  │ schedule_id → ctx0_schedules    │  │       │
│   │ └─────────────────────┘  │ state (JSONB checkpoint)        │  │       │
│   │                           └─────────────────────────────────┘  │       │
│   │                                                                  │       │
│   │ response_prompt, target_daemon_id, status, fire_count          │       │
│   └────────────────────────────────────────────────────────────────┘       │
│        │                                                                     │
│        │ Events (both types produce the same events)                        │
│        ▼                                                                     │
│   ctx0_trigger_events (EXISTING — no changes)                              │
│   ┌────────────────────────────────────────────────────────────────┐       │
│   │ id, trigger_id, payload, status, claimed_by, session_id       │       │
│   └────────────────────────────────────────────────────────────────┘       │
│                                                                              │
│   ctx0_schedules (EXISTING — linked for custom triggers)                   │
│   ┌────────────────────────────────────────────────────────────────┐       │
│   │ id, cron_expression, target_daemon_id, status                  │       │
│   │ settings: { is_trigger: true, trigger_id: '...' }             │       │
│   └────────────────────────────────────────────────────────────────┘       │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Event Flow

Native Composio Triggers

No changes from the current implementation. The only difference is the data source: ctx0_triggers instead of ctx0_composio_triggers.

External Service → Composio webhook → Bytespace webhook handler
    → INSERT ctx0_trigger_events (status: 'pending')
    → POST hub:/api/dispatch (wake up target daemon)
    → Daemon polls / receives wake-up
    → Claims event (lease-based, 120s)
    → Creates trigger session
    → Runs agent with event payload + response_prompt
    → Reports completion

Custom Triggers (Cron + Detection Script)

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CUSTOM TRIGGER EXECUTION FLOW                            │
│                                                                              │
│   Cron fires (via schedule system)                                         │
│        │                                                                     │
│        ▼                                                                     │
│   Schedule executor detects settings.is_trigger = true                     │
│        │                                                                     │
│        ▼                                                                     │
│   ┌─────────────────────────────────────┐                                   │
│   │ 1. LOAD TRIGGER + STATE             │                                   │
│   │                                     │                                   │
│   │ Fetch trigger row from ctx0_triggers│                                   │
│   │ Read: detection_script, state,      │                                   │
│   │       response_prompt               │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ▼                                                           │
│   ┌─────────────────────────────────────┐                                   │
│   │ 2. RUN DETECTION SCRIPT             │                                   │
│   │                                     │                                   │
│   │ Write state to /tmp/state.json      │                                   │
│   │ Execute: npx tsx detect.ts          │                                   │
│   │   --state /tmp/state.json           │                                   │
│   │                                     │                                   │
│   │ Script reads state, fetches data,   │                                   │
│   │ diffs, writes JSON result to stdout │                                   │
│   │                                     │                                   │
│   │ Timeout: 30s (configurable)         │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ├── FAILED (non-zero exit, timeout, invalid JSON)          │
│                  │   │                                                       │
│                  │   ├── DO NOT update state (preserve checkpoint)          │
│                  │   ├── Log error, increment consecutive_failures          │
│                  │   ├── If consecutive_failures >= 5 → auto-pause         │
│                  │   └── Retry on next cron tick                            │
│                  │                                                           │
│                  ▼                                                           │
│   ┌─────────────────────────────────────┐                                   │
│   │ 3. PARSE RESULT (success path)      │                                   │
│   │                                     │                                   │
│   │ stdout JSON:                        │                                   │
│   │ {                                   │                                   │
│   │   "fired": true,                    │                                   │
│   │   "events": [{ type, payload }],    │                                   │
│   │   "newState": { last_id: "130" }    │                                   │
│   │ }                                   │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ├── fired === false                                         │
│                  │   │                                                       │
│                  │   ├── UPDATE ctx0_triggers SET state = newState          │
│                  │   ├── INCREMENT poll_count                               │
│                  │   └── Done (no event, no LLM, $0)                       │
│                  │                                                           │
│                  └── fired === true                                          │
│                      │                                                       │
│                      ├── UPDATE ctx0_triggers SET state = newState          │
│                      ├── INCREMENT poll_count, fire_count                   │
│                      ├── INSERT ctx0_trigger_events (status: 'pending')    │
│                      │                                                       │
│                      ▼                                                       │
│   ┌─────────────────────────────────────┐                                   │
│   │ 4. EXECUTE RESPONSE                 │                                   │
│   │                                     │                                   │
│   │ Same path as native triggers:       │                                   │
│   │ • Create trigger session            │                                   │
│   │ • Inject response_prompt + payload  │                                   │
│   │ • Run agent                         │                                   │
│   │ • Report completion                 │                                   │
│   └─────────────────────────────────────┘                                   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Detection Script Contract

Detection scripts are regular executables. No framework, no special imports. The contract is just stdin/stdout JSON:

Input: State file path passed as --state argument. The file contains the previous state JSON (or {} on first run).

Output: JSON to stdout:

json
{ "fired": false, "newState": { "last_comment_id": "12347" } }

Or when triggered:

json
{ "fired": true, "events": [ { "type": "new_keyword_mentions", "payload": { "new_comments": [{ "id": "12348", "text": "Hello!", "user": "someone" }], "matched_keywords": ["hello"] } } ], "newState": { "last_comment_id": "12348" } }

Exit code: 0 = success (check fired field), non-zero = detection error.

API access for detection scripts: The daemon passes the proxy URL and session token via environment variables (PROXY_URL, SESSION_TOKEN). The script can make authenticated HTTP calls to Bytespace proxy endpoints (e.g., to execute Composio tools). This is the same trust boundary as any other daemon-executed script.

Example detection script (cron + Composio API):

typescript
#!/usr/bin/env npx tsx // detect.ts — Instagram keyword mention detector import { readFileSync } from 'fs'; const stateFile = process.argv.find((a, i) => process.argv[i - 1] === '--state') ?? ''; const state = JSON.parse(readFileSync(stateFile, 'utf-8')); const proxyUrl = process.env.PROXY_URL; const sessionToken = process.env.SESSION_TOKEN; // Config (passed via env or hardcoded during build) const keywords = (process.env.TRIGGER_KEYWORDS ?? '').split(',').map(k => k.trim().toLowerCase()); const accountId = process.env.TRIGGER_ACCOUNT_ID ?? ''; // Fetch current comments via Composio proxy const res = await fetch(`${proxyUrl}/api/proxy/composio/tools/execute/INSTAGRAM_GET_COMMENTS`, { method: 'POST', headers: { 'Authorization': `Bearer ${sessionToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ arguments: { account_id: accountId } }), }); const data = await res.json(); const comments = data.comments ?? []; // Diff against checkpoint const lastSeenId = state.last_comment_id ?? '0'; const newComments = comments.filter((c: any) => c.id > lastSeenId); const matches = newComments.filter((c: any) => keywords.some(kw => c.text.toLowerCase().includes(kw)) ); // Always update checkpoint const latestId = comments.reduce((max: string, c: any) => c.id > max ? c.id : max, lastSeenId); const newState = { last_comment_id: latestId }; if (matches.length === 0) { console.log(JSON.stringify({ fired: false, newState })); } else { console.log(JSON.stringify({ fired: true, events: [{ type: 'new_keyword_mentions', payload: { new_comments: matches, matched_keywords: [...new Set( matches.flatMap((m: any) => keywords.filter(kw => m.text.toLowerCase().includes(kw))) )] }, }], newState, })); }

The agent writes this script, runs it once to verify, then creates a schedule + trigger record to deploy it.

State Tracking Strategies

Different triggers need different state shapes. The agent chooses the right approach when building:

StrategyState shapeDiff logicUse case
Cursor{ last_id: "123" }id > last_idSequential data (comments, messages, feed items)
Hash{ content_hash: "sha256:..." }hash !== stored_hashPage content, DOM snapshots
Timestamp{ last_checked: "2026-02-28T..." }created_after > timestampTime-ordered APIs
Custom{ ... }Whatever the script needsComplex multi-field state

All strategies use the same ctx0_triggers.state JSONB column. The agent picks the right one based on the data source.


Two-Stage Filter Pattern

Some triggers need nuance that keyword matching can't provide. For example: "Notify me when someone says something negative about my brand on Instagram."

Keyword matching can't reliably detect sentiment. But running an LLM on every poll (288 times/day at 5-minute intervals) is expensive and violates the deterministic detection principle.

The solution is a two-stage filter: cheap programmatic detection gates expensive LLM judgment.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    TWO-STAGE FILTER                                          │
│                                                                              │
│   Stage 1: DETECTION (programmatic, $0)                                    │
│   ─────────────────────────────────────                                    │
│   Detection script polls Instagram API for new comments.                   │
│   Programmatic filter: keyword match, @mention detection, regex.           │
│   Catches broad candidates — some false positives are OK here.             │
│                                                                              │
│   "Does this comment contain any brand-related keywords?"                  │
│   • "your product sucks" → YES (contains "product")                       │
│   • "nice weather today" → NO (skip)                                       │
│   • "I hate bot0 support" → YES (contains "bot0")                         │
│                                                                              │
│   If any candidates found → trigger FIRES with candidates as payload.      │
│   If none → exit, $0 cost.                                                  │
│                                                                              │
│        │                                                                     │
│        ▼ (only runs when detection fires — not on every poll)              │
│                                                                              │
│   Stage 2: RESPONSE (LLM, ~$0.01-0.05)                                    │
│   ─────────────────────────────────────                                    │
│   Response prompt receives the candidate comments.                         │
│   LLM does the nuanced judgment:                                           │
│                                                                              │
│   "Analyze these comments. Are any genuinely negative about our brand?"    │
│   • "your product sucks" → NEGATIVE — draft response, alert user          │
│   • "I hate bot0 support" → NEGATIVE — escalate to support team           │
│                                                                              │
│   LLM can also decide to IGNORE false positives:                           │
│   • "I love your product, unlike competitors who suck" → POSITIVE (skip)  │
│                                                                              │
│   Cost is per-fire, not per-poll. If the brand gets mentioned              │
│   5 times/day, that's 5 LLM calls, not 288.                               │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

How the agent sets this up:

  1. User says: "Notify me when someone says something negative about my brand"
  2. Agent asks: "Where? Twitter? Instagram? Both?"
  3. User: "Instagram"
  4. Agent checks for native triggers → none for comment sentiment
  5. Agent builds detection script:
    • Polls INSTAGRAM_GET_COMMENTS via Composio
    • Filters for keywords related to the brand (user provides list)
    • Any keyword match = fire (broad net, some false positives OK)
  6. Agent writes response prompt:
    New comments mentioning your brand were detected on Instagram.
    
    {{event_payload}}
    
    Analyze each comment for sentiment. For genuinely negative comments:
    1. Summarize the issue
    2. Draft a professional reply
    3. Flag if it needs urgent attention
    
    Ignore false positives (positive/neutral mentions that happened
    to contain a keyword).
    
  7. The LLM judgment happens in the response phase — after the trigger fired, not during detection.

Key insight: The detection script's job is to be a cheap, broad filter. It's OK if it lets some false positives through — the response LLM handles the nuance. What matters is that detection never misses a real event (high recall) and costs $0 per poll.


Enforcing No-LLM Detection

Detection scripts must be purely programmatic. Here's how this is enforced at each level:

Structural Enforcement (Primary)

Detection scripts run as plain script executions (npx tsx detect.ts), not agent sessions. The script process has:

  • No AI SDK — no streamText(), no tool loop, no agent reasoning
  • No tool registry — no bash, write_file, or any daemon tool
  • No Anthropic/OpenAI client — no SDK imported, no client instantiated

To use an LLM, the agent building the trigger would have to manually write raw HTTP fetch() calls to the LLM proxy endpoint inside the detection script. This is technically possible but requires deliberate effort to circumvent the pattern.

Environment Enforcement (Phase 1)

The trigger runner passes a restricted set of environment variables to the detection script:

typescript
// What detection scripts receive: { PROXY_URL: '...', // For Composio/service API calls SESSION_TOKEN: '...', // Auth for proxy TRIGGER_*: '...', // Trigger-specific config (inputs) } // What detection scripts do NOT receive: // - No LLM_PROXY_URL (separate from PROXY_URL in future) // - No ANTHROPIC_API_KEY // - No OPENAI_API_KEY

Scoped Token Enforcement (Future — Phase 3)

Real infrastructure-level enforcement: the trigger runner requests a detection-scoped proxy token from Bytespace that only allows non-LLM endpoints:

Detection-scoped token allows:
  ✓ POST /api/proxy/composio/*    (Composio tool execution)
  ✓ GET  /api/proxy/composio/*    (Composio data fetching)
  ✗ POST /api/proxy/llm/*         (BLOCKED — 403)
  ✗ POST /api/proxy/db/*          (BLOCKED — 403)

If the detection script tries to call the LLM proxy, it gets a 403. This is the definitive enforcement mechanism, independent of what code the agent writes.

Convention Enforcement (Always)

The agent's system prompt, the trigger deploy tool description, and the trigger tool documentation all state:

Detection scripts must be deterministic. No LLM calls. No agent reasoning. Only data fetching and programmatic comparison. The LLM runs in the response phase after the trigger fires.

Combined with structural enforcement (no AI SDK available), this is sufficient for Phase 1. Scoped tokens add belt-and-suspenders for Phase 3.


Detection Failure Handling

Detection scripts can fail — API rate limits, network errors, auth expiration, script bugs, timeout. The trigger runner must handle these gracefully without losing events or corrupting state.

Failure Behavior

┌─────────────────────────────────────────────────────────────────────────────┐
│                    DETECTION FAILURE HANDLING                                │
│                                                                              │
│   Detection script fails (non-zero exit code or timeout)                   │
│        │                                                                     │
│        ▼                                                                     │
│   ┌─────────────────────────────────────┐                                   │
│   │ 1. DO NOT UPDATE STATE              │                                   │
│   │                                     │                                   │
│   │ CRITICAL: If detection failed       │                                   │
│   │ mid-way, we don't know the true     │                                   │
│   │ current state. Preserving the old   │                                   │
│   │ checkpoint means the next successful│                                   │
│   │ poll picks up where we left off.    │                                   │
│   │ No missed events.                   │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ▼                                                           │
│   ┌─────────────────────────────────────┐                                   │
│   │ 2. DO NOT FIRE                      │                                   │
│   │                                     │                                   │
│   │ No event created. No response       │                                   │
│   │ agent started. Failed detection     │                                   │
│   │ is a no-op (except logging).        │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ▼                                                           │
│   ┌─────────────────────────────────────┐                                   │
│   │ 3. LOG THE ERROR                    │                                   │
│   │                                     │                                   │
│   │ Record in trigger row:              │                                   │
│   │ • last_error (stderr or timeout msg)│                                   │
│   │ • last_error_at (timestamp)         │                                   │
│   │ • consecutive_failures (increment)  │                                   │
│   │ • Still increment poll_count        │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ▼                                                           │
│   ┌─────────────────────────────────────┐                                   │
│   │ 4. RETRY ON NEXT CRON TICK         │                                   │
│   │                                     │                                   │
│   │ The schedule keeps firing normally. │                                   │
│   │ Next poll runs the detection script │                                   │
│   │ again with the preserved state.     │                                   │
│   │ Transient errors self-heal.         │                                   │
│   └──────────────┬──────────────────────┘                                   │
│                  │                                                           │
│                  ▼                                                           │
│   ┌─────────────────────────────────────┐                                   │
│   │ 5. AFTER N CONSECUTIVE FAILURES     │                                   │
│   │    (default: 5)                     │                                   │
│   │                                     │                                   │
│   │ • Create improvement note with:     │                                   │
│   │   - Last N error messages           │                                   │
│   │   - Failure timestamps              │                                   │
│   │   - Script that failed              │                                   │
│   │                                     │                                   │
│   │ • Auto-pause the trigger            │                                   │
│   │   (set status = 'error_paused')     │                                   │
│   │                                     │                                   │
│   │ • Notify user via desktop           │                                   │
│   │   (if connected)                    │                                   │
│   │                                     │                                   │
│   │ Trigger stays paused until user     │                                   │
│   │ or agent fixes and resumes it.      │                                   │
│   └─────────────────────────────────────┘                                   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Error Classification

Error typeExampleBehavior
TransientNetwork timeout, API rate limit, 503Retry next tick. Usually self-heals.
Auth expired401 from Composio, expired tokenRetry next tick. If persists (3+ failures), likely needs re-auth. Improvement note: "Re-authenticate Instagram connection."
Script bugTypeError, undefined variableWon't self-heal. Consecutive failure threshold triggers pause + improvement note.
TimeoutScript exceeds 30s limitCould be transient (slow API) or permanent (infinite loop). Retry, pause after threshold.
Invalid outputstdout isn't valid JSON, missing fired fieldScript bug. Treated as failure — state preserved, improvement note after threshold.

Error Log

Each detection failure is appended to the error_log JSONB array on the trigger row. The array is capped at 20 entries — oldest entries are dropped when the cap is exceeded.

json
[ { "timestamp": "2026-02-28T10:00:00Z", "error": "401 Unauthorized — Instagram token expired", "stderr": "", "exit_code": 1, "duration_ms": 850 }, { "timestamp": "2026-02-28T10:05:00Z", "error": "401 Unauthorized — Instagram token expired", "stderr": "", "exit_code": 1, "duration_ms": 790 }, { "timestamp": "2026-02-28T10:10:00Z", "error": "TypeError: Cannot read property 'comments' of undefined", "stderr": "at detect.ts:42:15\n at processComments (detect.ts:38:5)", "exit_code": 1, "duration_ms": 1200 } ]

This gives the user and agent enough context to debug:

  • Same error every time → likely a permanent issue (auth expired, script bug)
  • Different errors → flaky dependency (intermittent API, rate limits)
  • First error differs from rest → cascading failure (one thing broke, then everything after it fails differently)

Error Tracking Behavior

Eventconsecutive_failureslast_errorerror_log
Successful pollReset to 0Set to NULLPreserved (not cleared — visible for post-recovery debugging)
Failed poll+1Set to error messageAppend entry (cap at 20)
5 consecutive failures5Last errorContains last 5 errors for improvement note
User resumes triggerReset to 0Set to NULLCleared (fresh start)
User resets stateUnchangedUnchangedUnchanged

When consecutive_failures >= 5:

  1. Auto-pause trigger (status = 'error_paused')
  2. Create improvement note containing the full error_log array
  3. Notify user via desktop (if connected)

The error log persists after recovery (successful polls) so the user can review what happened. It only clears when the user explicitly resumes a paused trigger — signaling they've addressed the issue.

The Golden Rule

Never update state on failure. This is the single most important rule. If the detection script crashed after fetching some data but before computing the diff, the state could be in an inconsistent position. By preserving the old checkpoint, the next successful run re-fetches everything from the last known-good point. The worst case is duplicate processing (which the response agent can handle), never missed events.


How the Agent Builds a Custom Trigger

No special builder mode. The agent uses its normal tools. Here's the typical flow:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    AGENT BUILDS A CUSTOM TRIGGER                            │
│                                                                              │
│   User: "Alert me when someone mentions 'hello' on my Instagram"           │
│        │                                                                     │
│        ▼                                                                     │
│   Step 1: CHECK NATIVE TRIGGERS                                            │
│        │                                                                     │
│        │  bytespace_trigger { action: "list_types", toolkit: "instagram" }  │
│        │  → No native trigger for comment mentions.                        │
│        │                                                                     │
│        ▼                                                                     │
│   Step 2: CHECK API TOOLS                                                   │
│        │                                                                     │
│        │  bytespace_search { query: "instagram comments" }                 │
│        │  → Found INSTAGRAM_GET_COMMENTS. Can poll via API.                │
│        │                                                                     │
│        ▼                                                                     │
│   Step 3: TELL USER THE PLAN                                               │
│        │                                                                     │
│        │  "Instagram doesn't have a native trigger for this.               │
│        │   I'll build a detection script that polls every 5 minutes        │
│        │   using the Instagram API, checks for new comments                │
│        │   matching 'hello', and alerts you when found.                    │
│        │   Shall I proceed?"                                                │
│        │                                                                     │
│        │  User: "Yes, go ahead"                                            │
│        │                                                                     │
│        ▼                                                                     │
│   Step 4: WRITE DETECTION SCRIPT                                           │
│        │                                                                     │
│        │  write_file: .bot0/triggers/instagram-mentions/detect.ts          │
│        │  (Regular file write — no special tool needed)                    │
│        │                                                                     │
│        ▼                                                                     │
│   Step 5: TEST THE SCRIPT                                                   │
│        │                                                                     │
│        │  bash: echo '{}' > /tmp/state.json &&                             │
│        │    PROXY_URL=... SESSION_TOKEN=... npx tsx detect.ts              │
│        │    --state /tmp/state.json                                         │
│        │                                                                     │
│        │  Output: { "fired": true, "events": [...], "newState": {...} }    │
│        │                                                                     │
│        │  "Detection script works! Found 3 comments with 'hello'.          │
│        │   Running again with updated state to verify no false fires..."   │
│        │                                                                     │
│        │  (Runs again with the newState from previous run)                 │
│        │  Output: { "fired": false, "newState": {...} }                    │
│        │                                                                     │
│        │  "Confirmed: no duplicate fires. Ready to deploy."               │
│        │                                                                     │
│        ▼                                                                     │
│   Step 6: DEPLOY                                                            │
│        │                                                                     │
│        │  trigger { action: "deploy", ... }                                │
│        │  → Creates ctx0_triggers row (type: 'cron_api')                  │
│        │  → Creates linked ctx0_schedules row (*/5 * * * *)               │
│        │  → Shows deployment confirmation (reuses schedule confirm gate)   │
│        │                                                                     │
│        │  User confirms → trigger is live.                                 │
│        │                                                                     │
│        ▼                                                                     │
│   Done. Trigger polls every 5 minutes on target daemon.                    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Key simplification: The agent doesn't need a special mode to build triggers. It uses write_file to write scripts, bash to test them, and the trigger tool to deploy. The "testing framework" is just... running the script twice and checking the output.


Trigger Management Tool

A single trigger tool handles all trigger operations (both native and custom):

typescript
{ name: "trigger", description: "Manage event triggers. Use this to create native triggers, deploy custom triggers, and control existing triggers.", inputSchema: { type: "object", properties: { action: { type: "string", enum: [ // Discovery "list", // List all triggers (native + custom) "get", // Get trigger details + recent fires "list_native_types", // List available Composio trigger types per toolkit // Creation "create_native", // Register a Composio native trigger "deploy", // Deploy a custom trigger (creates schedule + trigger row) // Control "pause", // Pause (stops cron / disables webhook) "resume", // Resume paused trigger "delete", // Delete trigger + schedule + events // Testing (custom triggers) "dry_run", // Run detection script, show what would fire "reset_state", // Clear state checkpoint (reprocess everything) // Daemon routing "network", // List available 24/7 daemons ] }, // For create_native trigger_slug: { type: "string", description: "Composio trigger slug" }, trigger_config: { type: "object", description: "Composio trigger config" }, // For deploy name: { type: "string", description: "Trigger name (lowercase-hyphenated)" }, description: { type: "string" }, detection_script_path: { type: "string", description: "Path to detection script" }, cron_expression: { type: "string", description: "Cron expression for polling" }, response_prompt: { type: "string", description: "What the agent does when trigger fires" }, initial_state: { type: "object", description: "Initial state checkpoint. If omitted, deploy runs a seeding poll: detection script executes once with {} state, captures newState, but does NOT fire. This prevents the first real poll from treating all historical data as new." }, // Shared trigger: { type: "string", description: "Trigger name or ID (for get/pause/resume/delete/dry_run)" }, target_daemon: { type: "string", description: "Daemon ID/name/alias" }, toolkit: { type: "string", description: "Service name (gmail, instagram, etc.)" }, }, required: ["action"] } }

This replaces the current bytespace_trigger tool. One tool for all trigger operations.


Deployment & Daemon Assignment

Custom triggers are powered by cron schedules. When a trigger is deployed:

  1. ctx0_triggers row created — with type, detection_script, cron_expression, state, response_prompt
  2. ctx0_schedules row created — linked via schedule_id FK. The schedule's settings contains { is_trigger: true, trigger_id: "..." }
  3. State seeded — If initial_state is provided, use it directly. Otherwise, the deploy action runs a seeding poll: execute the detection script once with {} state, capture newState from the output, but do NOT fire even if fired: true. This sets the checkpoint to "now" so the first real cron tick only detects genuinely new changes, not all historical data.

The schedule executor checks settings.is_trigger. If true, it runs the trigger runner instead of a regular agent prompt. This avoids the @trigger: prefix hack — it's a clean flag in the settings JSONB.

Daemon Routing

  • Custom triggers require an agent-role daemon (24/7). Daily-drivers are rejected.
  • Native triggers target a specific daemon via target_daemon_id. If the daemon is offline, events queue in ctx0_trigger_events and are processed when the daemon comes back online. No events are lost.
  • The trigger network action lists available 24/7 daemons (same as schedule network).

What Happens When the Target Daemon Is Offline

Native trigger fires → Event queued in ctx0_trigger_events → Daemon comes online → Poller catches up

Custom trigger cron fires → Schedule executor on target daemon... but daemon is offline?
  → The cron won't fire because QStash/scheduler can't reach the daemon
  → Events are missed during downtime
  → When daemon comes back, next cron tick catches new changes (state-based)
  → Some events may be lost depending on the data source's retention

Future: auto-failover to another online agent daemon

Comparison: Triggers vs Schedules

┌─────────────────────────────────────────────────────────────────────────────┐
│                    TRIGGERS vs SCHEDULES                                    │
│                                                                              │
│                          Trigger                    Schedule                │
│   ──────────            ─────────                  ────────                │
│   Runs when             Event detected              Cron fires             │
│   Detection             Script or webhook           N/A (time-based)       │
│   Has state             Yes (JSONB on row)          No                     │
│   Response              Prompt + agent              Prompt + agent         │
│   Daemon target         Required (24/7)             Required (24/7)        │
│   Database              ctx0_triggers               ctx0_schedules         │
│   Testing               Dry run before deploy       Manual run             │
│                                                                              │
│   Key distinction:                                                          │
│   • Schedule = "do X every Y time"                                         │
│   • Trigger = "do X when Y changes"                                        │
│                                                                              │
│   A custom trigger = schedule + detection script + state checkpoint.       │
│   The schedule provides the timing. The trigger adds change detection.     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Trigger Improvement Notes

Same pattern as skills. When a trigger's detection script fails or produces false positives, the executing agent documents the issue:

typescript
interface TriggerImprovementNote { id: string; timestamp: string; sessionId: string; daemonId: string; phase: 'detection' | 'response'; // Which part failed error: string; diagnosis: string; suggestedFix: string; status: 'open' | 'dismissed' | 'resolved'; }

Stored in ctx0_triggers.improvement_notes (JSONB array). The user sees notification badges on triggers with open notes and can ask the agent to fix them (agent reads the note, modifies the detection script, re-tests, re-deploys).


Security Considerations

Detection Script Execution

Detection scripts run on the daemon with the same trust level as any agent-executed code. The no-LLM constraint is enforced structurally, environmentally, and by convention — see Enforcing No-LLM Detection for the full enforcement stack. Detection failure handling preserves state safety — see Detection Failure Handling.

Response Agent Permissions

ModeUse caseBehavior
dangerously_autoHigh-trust, well-tested triggersAuto-approves all tool calls
autoDefaultBlocks on dangerous commands
askSensitive actionsEvery tool call needs user approval (requires connected desktop)

State Security

  • State is stored per-user in Supabase with RLS
  • State is scoped to individual trigger instances
  • State is JSON-serializable (no executable content)
  • State reset is a simple SET state = '{}'

Implementation Roadmap

Phase 1 — Unified Table + Native Trigger Migration

TaskComplexity
Create ctx0_triggers Drizzle schemaMedium
Migrate ctx0_composio_triggers data → ctx0_triggersLow
Update trigger poller to read from ctx0_triggersLow
Update trigger executor to read response_prompt from ctx0_triggersLow
Update webhook handler to write to ctx0_triggersLow
Drop ctx0_composio_triggersLow
Update bytespace_trigger tool → unified trigger toolMedium

Phase 2 — Custom Trigger Infrastructure

TaskComplexity
Trigger runner (schedule executor recognizes is_trigger flag)Medium
Detection script execution (write state file, run script, parse stdout JSON)Medium
State read/write on ctx0_triggers.state columnLow
trigger deploy action (create trigger row + linked schedule)Medium
trigger dry_run action (run detection without firing response)Low
Deploy confirmation gate (reuse ScheduleConfirmationManager)Low
Bytespace proxy CRUD endpoints for triggersMedium

Phase 3 — UI & Monitoring

TaskComplexity
Triggers section in NetworkSidebar (unified list)Medium
Trigger fire history (query ctx0_trigger_events per trigger)Low
Improvement notes display + badgeMedium
trigger reset_state actionLow
Trigger pause/resume/deleteLow

Phase 4 — Advanced Detection Methods

TaskComplexity
dom0-based detection scripts (browser automation)High
cmd0-based detection scripts (visual grounding)High
Auto-failover to backup daemon on downtimeHigh