External triggers.

Five trigger paths matter. Desktop chat, mobile chat, and routines are real today. Slack-style inbound messages and public webhooks are product work, not shipped features.

1. The user sends a message from the desktop

Standard chat. The desktop UI calls POST /v1/agents/<path>/sessions. The engine spawns a CLI subprocess, the response streams back. Chapter 4 covers it. Shipped.

2. The user sends a message from mobile

Mobile is a PWA at tunnel.gethouston.ai. The desktop engine dials an outbound WebSocket to the Houston Relay (houston-relay/, Cloudflare Worker plus Durable Object). The relay proxies mobile HTTP and WS over that link. Same engine route as desktop chat. Shipped.

3. A Slack-style inbound message arrives

This is not shipped today. Houston can let agents use Slack through Composio tools, and the UI has generic channel concepts, but there is no dedicated engine Slack crate, route, or two-way sync handler in the current repo.

Future shape: an inbound channel event maps to one agent session, writes a trigger_runs row, starts a turn through the same durable turn path as chat, and posts the answer back through the channel adapter.

4. A cron fires

Each agent has routines. 0 7 * * * runs at 7am every day. Today the active scheduler lives inside engine/houston-engine-core/src/routines/scheduler.rs. It sleeps until the fire time and calls run_routine through a RoutineDispatcher.

Current routine run history exists in the routines data model, but there is no global SQL trigger_runs ledger tied to durable turn replay. Chapter 6 adds that ledger and makes missed-fire policy explicit.

5. A webhook arrives

Stripe sends a payment event. GitHub sends a PR notification. Each one routed to a specific agent.

Honest status: engine/houston-events has typed input scaffolding, including Webhook, plus an unbounded queue. That crate is not the central path for current chat or routines. There is no public HTTP route, webhook token table, signature verification, handler, durable trigger row, or session dispatch integration.

The route: POST /v1/hooks/<route_token> in a new engine/houston-engine-server/src/routes/hooks.rs. The handler must verify the hook token and provider signature, insert trigger_runs, then start a durable turn.

Per-hook tokens, not the bearer token

Each webhook gets its own route token, separate from the engine bearer. Stripe's token only fires the Stripe hook. A compromised Stripe token lets the attacker fake payment events but doesn't let them read your chat history or trigger other agents.

Bounded queue (M1 prerequisite)

The old event queue is unbounded (mpsc::unbounded_channel at engine/houston-events/src/queue.rs:36). A webhook flood must never be allowed to balloon memory. M1 either wires a bounded queue into the real trigger path or implements bounded intake at the route layer, with explicit accept/reject audit rows. Without that change, opening POST /v1/hooks/... to the world is a DoS waiting to happen.

Wake-on-event for sleeping engines (M4)

Once the engine learns to sleep in Cloud (Chapter 10), the relay in front will catch inbound triggers, wake the engine via the platform API (Fly Machines, systemd socket activation, etc), wait for /v1/health to respond, then proxy the request through.

Human chat, mobile chat, Slack-style inbound, webhook, and scheduled routines should converge on the same durable acceptance path. Routines may be woken by a central scheduler instead of a relay request, but they still need the same trigger_runs and turn ledger.

Where to look in the code

Webhook input scaffolding: engine/houston-events/src/types.rs. Old queue, unbounded today: engine/houston-events/src/queue.rs:36. Active routines scheduler: engine/houston-engine-core/src/routines/scheduler.rs. Routine runner: engine/houston-engine-core/src/routines/runner.rs. Relay: houston-relay/. Missing pieces for webhooks: route, token model, signature verification, bounded intake, trigger_runs, and durable session dispatch.