Governed Triggers
A Touba trigger turns an event into a governed execution.
Two V1 paths are available:
schedule -> strategy
request -> strategy
The first uses a bounded foreground runner. The second uses an explicit HTTP webhook service with a durable queue, TD-1 state, and trace. Both paths go through AIR, manifest, and runtime checks before the strategy is executed.
1. Write the Program
module examples.daily_research
strategy DailyResearch
takes topic as Text
do
show
with message as "Daily research run: " ++ topic
trigger DailyResearchSchedule
on schedule "every 1s"
runs strategy DailyResearch
with topic as "daily review"
limits
max_runs_per_day as 1
timeout_ms as 30000
trigger DailyResearchWebhook
on request POST "/webhook/daily-research"
runs strategy DailyResearch
with topic as payload.topic
limits
max_runs_per_day as 10
timeout_ms as 30000
The runner accepts this format:
every <n>s
every <n>m
every <n>h
Full cron support belongs to a later scheduling layer.
2. Compile
touba compile src/examples/daily_research.tb --out daily_research.air.json
AIR contains the trigger declaration, its target, and its limits. It contains no scheduler, secret, or external payload.
payload.topic binds the event field dynamically; it is neither a dummy value
nor a fallback. The manifest mapping must cover this argument; missing fields
are refused before execution.
3. Activate the Trigger in the Manifest
{
"manifest_version": "touba-manifest-v1",
"bindings": {},
"triggers": {
"trigger:examples.daily_research/DailyResearchSchedule": {
"enabled": true,
"source": {
"kind": "schedule"
},
"limits": {
"max_runs_per_day": 1,
"timeout_ms": 30000
}
},
"trigger:examples.daily_research/DailyResearchWebhook": {
"enabled": true,
"source": {
"kind": "request"
},
"payload": {
"format": "json",
"content_types": ["application/json"],
"max_bytes": 4096,
"map": {
"topic": "$.topic"
}
},
"limits": {
"max_runs_per_day": 10,
"timeout_ms": 30000
}
}
}
}
Without this entry, the runtime refuses the trigger before executing the strategy.
4. Run the Real Trigger
touba trigger run daily_research.air.json \
--manifest touba.manifest.json \
--trigger trigger:examples.daily_research/DailyResearchSchedule \
--max-runs 1 \
--state touba.triggers.sqlite \
--trace-out daily_research.trace.json
Expected result after about one second:
Daily research run: daily review
--max-runs sets the number of runs. The default SQLite ledger is
.touba/triggers.sqlite, in the project or next to the AIR file outside a project.
--state selects another .sqlite file.
The trace written to daily_research.trace.json contains the received event, trigger acceptance, execution start, and run completion.
The SQLite ledger records consumed event_ids, observed refusals, and the UTC day key
used to apply max_runs_per_day, without raw payloads. If the same event is
replayed, Touba refuses before relaunching the strategy with
TriggerReplayRejected. An acceptance recorded on a previous day does not block
the first event of the current day. The path passed to --state must end with
.triggers.sqlite.
5. Run the Webhook Service
touba trigger serve daily_research.air.json \
--manifest touba.manifest.json \
--state touba.triggers.sqlite \
--queue touba.trigger-queue.sqlite \
--trace-dir traces \
--max-events 1
In another terminal:
curl -X POST http://127.0.0.1:8787/webhook/daily-research \
-H 'content-type: application/json' \
-H 'x-touba-event-id: evt-webhook-001' \
-d '{"topic":"industrial adoption"}'
The service returns 202 queued, appends an entry to
touba.trigger-queue.sqlite, executes the strategy, writes TD-1 state, and
produces one trace per entry when --trace-dir is provided. The raw HTTP body is
not injected as implicit Touba input and is not written to the queue, state, or
trace; the queue keeps the payload size, the SHA-256 digest, and the arguments
explicitly mapped by the manifest. Fields not listed in payload.map are
ignored.
For a webhook exposed to an external provider, add
--signature-secret-env TOUBA_TRIGGER_SIGNATURE_SECRET and send
x-touba-signature: sha256=<hmac> computed over the exact HTTP body.
On a non-loopback host, add --auth-token-env TOUBA_TRIGGER_TOKEN and send
Authorization: Bearer <token> or x-touba-trigger-token: <token>.
6. Test Checks Without Executing
touba run daily_research.air.json \
--manifest touba.manifest.json \
--trigger-event-json '{"trigger_ref":"trigger:examples.daily_research/DailyResearchSchedule","event_id":"evt-001","source_kind":"schedule"}' \
--trigger-dry-run
Expected result:
- the trigger exists in AIR;
- the manifest activates it;
- the event source matches;
- manifest limits do not broaden AIR limits;
- the target strategy is not launched in dry run.
7. Execute with an Explicit Event
touba run daily_research.air.json \
--manifest touba.manifest.json \
--trigger-event-json '{"trigger_ref":"trigger:examples.daily_research/DailyResearchSchedule","event_id":"evt-002","source_kind":"schedule"}' \
--output trace
This mode is used for host tests or adapters. It supplies the event manually instead of waiting for the declared interval.
8. Inspect
touba inspect daily_research.air.json
touba inspect trace.json
touba trigger state touba.triggers.sqlite --trigger trigger:examples.daily_research/DailyResearchSchedule
touba inspect daily_research.air.json --manifest touba.manifest.json
Inspection displays what exists in the supplied artifacts. For durable trigger
state (TD-1), it displays acceptance counts, refusal counts, and consumed
event_ids. The webhook service exists when it is explicitly started with
touba trigger serve; deployment supervision remains the responsibility of the
host environment.
Retention and migration
Trigger admission is committed before its strategy starts. A process crash therefore preserves the consumed identity. Daily counts use UTC; replay identifiers remain stored after archiving.
The ledger defaults to a 512 MiB limit. --state-max-bytes adjusts this limit
(minimum 65,536 bytes). A full ledger blocks new admissions.
touba trigger state touba.triggers.sqlite --limit 100
touba trigger state touba.triggers.sqlite --after 100 --limit 100
Use the returned next value as the next page cursor.
--trigger <reference> filters events and includes that trigger’s counters.
touba trigger archive-state touba.triggers.sqlite --before 2026-10-01T00:00:00Z
touba trigger archive-state touba.triggers.sqlite --before 2026-10-01T00:00:00Z \
--out archives/triggers-october.sqlite --apply
The first command previews. The second copies and verifies the archive before
removing successful-event details and refusals. Active, paused and uncertain
events, counters and consumed identities stay in the ledger.
Read an archive with touba trigger state.
To convert an older JSON state, stop its service first:
touba trigger migrate-state old.trigger-state.json --out touba.triggers.sqlite
touba trigger migrate-state old.trigger-state.json --out touba.triggers.sqlite --apply
Migration creates a new ledger and an exact .source-backup.json copy.
It preserves the original file and refuses to overwrite an existing destination.
Migrated admissions keep an uncertain outcome because the old ledger did not
record the final result. Migration never runs an event.
Useful Refusals
| Situation | Expected behavior |
|---|---|
| Trigger absent from AIR | refusal before execution |
| Trigger disabled in the manifest | refusal before execution |
| Different event source | refusal before execution |
| Manifest broadens an AIR limit | refusal before execution |
Missing event_id | refusal before execution |
event_id already consumed in --state | replay refusal before execution |
Webhook service without --queue | SQLite queue at .touba/trigger-queue.sqlite |
| Request payload that violates the manifest contract | refusal before enqueue |
| Invalid webhook signature | refusal before enqueue |
| Webhook service exposed outside loopback without token | CLI refusal before startup |
Runtime Contract
- The foreground runner executes governed schedule entries.
- The webhook service executes
request -> strategyentries. - The durable queue follows
touba-trigger-queue-v1. - The manifest activates each trigger explicitly.
- Trigger state records consumed event ids and replay refusals.
- Manifest payload contracts map explicit JSON fields to target strategy arguments.
- HMAC signatures, bounded retries, stale
processingrecovery, and dead-letter states are supported by the webhook service. - Distributed schedulers, distributed queues, cloud adapters, manifest auto-discovery, direct agent triggering, and regulatory certification are surrounding platform layers.