Skip to main content
A trigger adapter turns one wake-up mechanism into an agent run: an inbound webhook, a clock, or a Slack event. Import them from the @lousho/build-ai-agent/triggers subpath: WebhookTriggerAdapter, SlackTriggerAdapter, CronTriggerAdapter and TriggerRegistry. Wire any of them with listen(agent, onEvent), where onEvent runs the agent, and call stop() on the returned handle to shut the adapter down.

Choosing between triggers, channels and schedules

Cron exists in both triggers and schedules today. New projects that use agent directories or lousho build should prefer defineSchedule(); this is guidance, and CronTriggerAdapter keeps working. WebhookTriggerAdapter uses webhookChannel() for its authentication.

The TriggerAdapter interface

listen takes both agent and onEvent on purpose. agent is the plain “prompt in, text out” surface, and onEvent is the entry point the adapter calls when its trigger fires: a simple caller passes (input) => agent.send(input), while a caller with checkpointing, approval gates or abort control passes an onEvent that wraps all of that. Reply semantics differ per adapter. A webhook writes the result you return as the HTTP response, so it has no reply(). A cron has no caller waiting, so it hands the result to its onResult option. Slack posts the answer back through reply(). Write your own adapter by returning a handle from listen:
TriggerRegistry keeps adapters by name: register(name, adapter), registerMany(), get(), has(), list(), getAll(), unregister(), clear() and size(). globalTriggerRegistry is a shared instance.

Webhook

Webhook authentication

WebhookTriggerAdapter starts an HTTP server. Always set auth for a webhook that is reachable from outside your machine: without it, anyone who can reach the port can run your agent (and spend your tokens). If you listen on a non-loopback host with no auth, the adapter logs a one-time warning through options.logger.
HMAC options: header (default x-signature-256), algorithm (sha256 or sha1, default sha256), prefix (default sha256=; '' for a bare digest), timestampHeader and toleranceSeconds. Signatures and bearer tokens are compared in constant time. A request that fails authentication gets a generic 401 {"error":"Unauthorized"} - the response never says which check failed - and the reason (never a secret or signature) is logged at warn level. Serve webhooks over HTTPS (terminate TLS in front of the adapter) so tokens and payloads are not sent in clear text.

Slack

Slack request signatures

SlackTriggerAdapter.handleRequest({ headers, rawBody }) handles a raw Slack Events API request and returns the { status, body } to send back. Set signingSecret for any endpoint reachable from outside your machine: without it, anyone who can reach the endpoint can run your agent, and listen() logs a one-time warning through options.logger. With it, every request is verified as Slack documents before the body is parsed: HMAC-SHA256 over v0:{X-Slack-Request-Timestamp}:{raw body}, compared in constant time with X-Slack-Signature (v0=<hex>), and requests more than five minutes old are rejected. Failures get a generic 401 {"error":"Unauthorized"}; the reason (never a secret or signature) is logged at warn level. The signed url_verification handshake is answered after verification.
handleRequest answers Slack after the agent finishes; Slack expects a reply within three seconds, so for slow agents acknowledge first and run the agent in the background.

Cron

Cron schedules

CronTriggerAdapter takes either a fixed intervalMs or a real cron expression:
Supported syntax: *, lists (1,15), ranges (1-5), steps (*/15, 10-40/10), month names (JAN) and weekday names (MON), with 0 and 7 both meaning Sunday, plus @hourly, @daily, @weekly and @monthly. As in classic cron, when both day-of-month and day-of-week are restricted a day matches if either does. An invalid expression throws a CronExpressionError naming the field and showing a valid example. parseCronExpression(expr, timezone).nextRun(after) is exported if you need the next fire time. Around daylight-saving changes, a time that does not exist (spring forward) is skipped for that day, and a time that happens twice (fall back) fires once; an every-hour schedule keeps firing hourly. The timer is re-armed after each run from the scheduled time (no drift, no double fire), and stop() clears it.