Governed Real Trigger: Morning Review
This tutorial shows how to launch a Touba execution automatically with a
schedule -> strategy trigger. The scenario focuses on triggering, limits and
traces before adding real tools.
You will build a morning review that starts after roughly one second, displays two messages, returns a title, and writes a trace.
What You Will Learn
| Step | What You Verify |
|---|---|
.tb source | the trigger declares when to launch a strategy and with which arguments |
| AIR | the trigger becomes a runtime artifact, not a comment |
| Manifest | the host explicitly activates the authorized trigger |
| Runner | touba trigger run waits for the interval, then launches the strategy |
| Durable trigger state | touba.triggers.sqlite keeps consumed event_id values |
| Trace | the runtime records reception, acceptance, start, and completion |
| Refusal | a replay or disabled trigger is refused before execution |
1. Create the Directory
mkdir -p morning-trigger-demo
cd morning-trigger-demo
Then create the source file.
module examples.trigger_morning_review
define ReviewTopic as Text = "signaux produit Touba"
define RiskTheme as Text = "risques agents"
function BuildReviewTitle
takes topic as Text
gives Text
do
give "Revue matinale: " ++ topic
skill BuildMorningBrief
description "Build and display the morning review for the supplied topic."
takes topic as Text
gives Text
do
BuildReviewTitle
with topic as topic
into title
show
with message as title
show
with message as "Point de controle: verifier traces, refus et prochaines actions"
give title
strategy RunMorningReview
takes topic as Text
gives Text
uses BuildMorningBrief
do
bind scoped_topic as Text = topic ++ " / " ++ RiskTheme
BuildMorningBrief
with topic as scoped_topic
into title
give title
trigger MorningReview
on schedule "every 1s"
runs strategy RunMorningReview
with topic as ReviewTopic
limits
max_runs_per_day as 1
timeout_ms as 30000
This file contains four distinct ideas:
definenames non-sensitive module values;functionbuilds a title without external effects;skilldisplays the result and returns a value;triggerlaunches the strategy on a bounded interval.
The trigger uses the foreground runner standard:
every <n>s
every <n>m
every <n>h
The runner does not execute full cron syntax.
2. Compile to AIR
touba compile morning-review.tb --out morning-review.air.json
Nothing is executed at this stage. Compilation produces AIR: the contract consumed by the runtime.
You can inspect AIR:
touba inspect morning-review.air.json
You should see a RunMorningReview strategy and a MorningReview trigger.
3. Activate the Trigger
A Touba trigger is not active only because it exists in source. The host must authorize it in a manifest.
Create touba.manifest.json:
{
"manifest_version": "touba-manifest-v1",
"triggers": {
"trigger:examples.trigger_morning_review/MorningReview": {
"enabled": true,
"source": {
"kind": "schedule"
},
"limits": {
"max_runs_per_day": 1,
"timeout_ms": 30000
}
}
}
}
The manifest states three things:
- this trigger is enabled in this environment;
- the authorized source is
schedule; - host limits do not widen the limits declared in
.tb.
4. Launch the Real Trigger
touba trigger run morning-review.air.json \
--manifest touba.manifest.json \
--trigger trigger:examples.trigger_morning_review/MorningReview \
--max-runs 1 \
--state touba.triggers.sqlite \
--trace-out morning-review.trace.json
The command waits for the every 1s interval, generates a normalized event, then
launches the strategy.
Expected output:
Morning review: Touba product signals / agent risks
Checkpoint: verify traces, refusals, and next actions
The runner stops after one run because --max-runs 1 is explicit. Without
--max-runs, the CLI refuses to start. Without --state, the ledger is created
at .touba/triggers.sqlite.
--state creates or updates durable trigger state. This state contains no raw
payload; it keeps only event_id values, accepted/refused statuses, refusal
reasons, and the UTC day key used to apply max_runs_per_day. The path passed
to --state must end with .triggers.sqlite.
5. Read the Trace
The trace is written to morning-review.trace.json.
touba inspect morning-review.trace.json
touba trigger state touba.triggers.sqlite --trigger trigger:examples.trigger_morning_review/MorningReview
The trace should make these events visible:
trigger_event_received
trigger_event_accepted
trigger_execution_started
user_interaction_shown
trigger_execution_completed
run_completed
This point matters: terminal output is not the only evidence. The trace remains the audit artifact for the run.
Inspecting touba.triggers.sqlite should show one trigger, one acceptance,
and one consumed event_id.
6. Refuse an Explicit Replay
The runner produces its own event_id values. To test replay in a controlled
way, send the same explicit event twice with state dedicated to this test. Use a
separate file so you do not reuse the quota already consumed by the runner.
First pass:
touba run morning-review.air.json \
--manifest touba.manifest.json \
--trigger-event-json '{"trigger_ref":"trigger:examples.trigger_morning_review/MorningReview","event_id":"evt-demo-replay","source_kind":"schedule"}' \
--state replay.triggers.sqlite \
--output trace
Second pass with the same event_id:
if touba run morning-review.air.json \
--manifest touba.manifest.json \
--trigger-event-json '{"trigger_ref":"trigger:examples.trigger_morning_review/MorningReview","event_id":"evt-demo-replay","source_kind":"schedule"}' \
--state replay.triggers.sqlite \
--output json > replay-result.json; then
exit 1
else
test "$?" -eq 1
fi
node --input-type=module -e 'import assert from "node:assert/strict"; import {readFileSync} from "node:fs"; const r=JSON.parse(readFileSync("replay-result.json","utf8")); assert.equal(r.reason,"TriggerReplayRejected"); assert.ok(!r.trace.events.some(e=>e.kind==="trigger_execution_started")); console.log("TRIGGER_REPLAY_REFUSED_OK");'
Expected result:
trigger error [TriggerReplayRejected]
The strategy is not launched again. The trace contains trigger_event_refused
with reason event_replay.
7. Test Checks Without Executing
Dry run verifies the trigger, manifest, and limits without launching the strategy.
touba run morning-review.air.json \
--manifest touba.manifest.json \
--trigger-event-json '{"trigger_ref":"trigger:examples.trigger_morning_review/MorningReview","event_id":"evt-demo-001","source_kind":"schedule"}' \
--trigger-dry-run \
--output trace
You should get a trace of accepted checks. No show message is displayed because
the strategy is not executed in dry run.
8. Produce an Intentional Refusal
Create a disabled manifest:
{
"manifest_version": "touba-manifest-v1",
"triggers": {
"trigger:examples.trigger_morning_review/MorningReview": {
"enabled": false,
"source": {
"kind": "schedule"
},
"limits": {
"max_runs_per_day": 1,
"timeout_ms": 30000
}
}
}
}
Then run:
if touba trigger run morning-review.air.json \
--manifest touba.manifest.disabled.json \
--trigger trigger:examples.trigger_morning_review/MorningReview \
--max-runs 1 \
--state touba.triggers.sqlite \
--output json > disabled-result.json; then
exit 1
else
test "$?" -eq 1
fi
node --input-type=module -e 'import assert from "node:assert/strict"; import {readFileSync} from "node:fs"; const r=JSON.parse(readFileSync("disabled-result.json","utf8")); assert.equal(r.reason,"TriggerDisabled"); assert.deepEqual(r.messages,[]); console.log("DISABLED_TRIGGER_REFUSED_OK");'
Expected result:
trigger error [TriggerDisabled]
The runtime refuses before entering RunMorningReview. No show message is
produced.
9. What You Actually Tested
This tutorial tests a first reactive autonomy path:
- the Touba program declares the trigger;
- the environment explicitly authorizes that trigger;
- the foreground runner waits for a real interval;
- durable trigger state refuses an explicit replay;
- the runtime applies checks before execution;
- the trace documents acceptance or refusal.
Next Step
For request -> strategy triggers with the HTTP webhook service, persistent
queue, durable state, HMAC signature, and dead-letter handling, see
Governed Triggers.
This scenario establishes the schedule -> strategy model: declared trigger,
durable state, replay refusal, and usable trace.
For several scheduled runs, --output json returns an array of results with each run’s status and reason. The aggregate trace has its own run ID; origin_run_id and origin_event_id preserve child-event provenance. trigger_runner_run_started and trigger_runner_run_completed delimit each child. The runner stops at the first pause, stop, cancellation or failure; it does not silently continue or report aggregate success.