@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.
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:
*, 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.