Skip to main content
Version: 1.0.0

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​

StepWhat You Verify
.tb sourcethe trigger declares when to launch a strategy and with which arguments
AIRthe trigger becomes a runtime artifact, not a comment
Manifestthe host explicitly activates the authorized trigger
Runnertouba trigger run waits for the interval, then launches the strategy
Durable trigger statetouba.triggers.sqlite keeps consumed event_id values
Tracethe runtime records reception, acceptance, start, and completion
Refusala replay or disabled trigger is refused before execution

1. Create the Directory​

Terminal
mkdir -p morning-trigger-demo
cd morning-trigger-demo

Then create the source file.

morning-review.tb
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:

  • define names non-sensitive module values;
  • function builds a title without external effects;
  • skill displays the result and returns a value;
  • trigger launches the strategy on a bounded interval.

The trigger uses the foreground runner standard:

Available formats
every <n>s
every <n>m
every <n>h

The runner does not execute full cron syntax.

2. Compile to AIR​

Terminal
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:

Terminal
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:

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​

Terminal
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:

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.

Terminal
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:

Expected events
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:

Terminal
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:

Terminal
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:

Error
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.

Terminal
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:

touba.manifest.disabled.json
{
"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:

Terminal
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:

Error
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.