Triggers¶
A trigger connects "something happened" to "run this automation." It realizes Victor's trigger → condition → action pattern: when the trigger fires, its optional condition is checked, and if the condition holds, the action runs a named automation with an input payload.
There are three kinds of trigger:
| Type | Fires when… | Typical use |
|---|---|---|
cron |
a schedule elapses | nightly cleanup, daily reminders, periodic follow-ups |
webhook |
an external system POSTs to a URL |
inbound events from another app (a new ticket, a paid invoice) |
manual |
it is not fired automatically | declaring a trigger → automation binding that is neither scheduled nor exposed as an endpoint |
Triggers are pure data: you author them as YAML files inside a module and list them in module.yaml. You don't write any wiring code — installing the module registers the trigger, and (for cron) schedules it and (for webhook) exposes its endpoint.
Triggers vs. automations vs. workflows
A trigger decides when something runs. An automation is what runs — a small guarded sequence of steps. A workflow is a longer, stateful process an automation can drive. Keep the trigger thin; put the real logic in the automation it calls.
Where triggers live¶
Put trigger YAML files under a triggers/ directory in your module and declare them with the triggers: key in module.yaml:
helpdesk/
module.yaml
models/
views/
automations/
escalate.yaml
triggers/
daily_escalation.yaml
ticket_hook.yaml
# helpdesk/module.yaml
name: helpdesk
version: 0.1.0
requires: [base, contact]
models: [models]
automations: [automations/escalate.yaml]
triggers: # list of trigger YAML file paths
- triggers/daily_escalation.yaml
- triggers/ticket_hook.yaml
Each file under triggers: becomes one trigger when the module installs. See Module structure & manifest for the full manifest reference.
Trigger schema¶
A trigger file is a small YAML document. The fields are the same across all three types; only a few are type-specific.
# triggers/daily_escalation.yaml — a cron trigger
name: daily_escalation
label: Daily ticket escalation
type: cron
cron: "0 9 * * *" # required for type: cron
automation: escalate # the automation to run, by name
condition: "true" # optional; runs only when truthy
enabled: true # optional; default true
Field reference¶
| Field | Required | Applies to | Description |
|---|---|---|---|
name |
yes | all | Technical id, unique within the module. |
label |
no | all | Human-friendly name shown in the app. |
type |
yes | all | One of cron, webhook, manual. |
cron |
yes (cron only) | cron |
A crontab expression, e.g. "0 9 * * *" for every day at 09:00. |
automation |
yes | all | Name of the automation to run when the trigger fires. |
condition |
no | all | Expression checked at fire time; if falsy, the automation is skipped. See Conditions. |
enabled |
no | all | Whether the trigger runs. Defaults to true. |
The automation must exist
The automation name is resolved when the module installs. If no automation by that name is defined (in this module or one it requires), the install fails fast — a helpful way to catch typos early.
Conditions¶
Every trigger may carry an optional condition: a small, restricted expression evaluated when the trigger fires. The incoming payload is available as input.
- empty or omitted → the trigger always fires.
- truthy → the automation runs.
- falsy or errors → the automation is skipped (and the trigger is not marked as fired).
You get read-only access to the payload plus comparisons, boolean logic, and simple arithmetic:
| Category | Allowed |
|---|---|
| Field / key access | input.amount, input['customer_id'] |
| Comparisons | > < == != >= <= |
| Boolean logic | and or not |
| Membership | in not in |
| Arithmetic | + - * / |
| Literals | true, false, null, numbers, strings, and inline list/tuple literals like ['high', 'urgent'] |
# Escalate when the priority is one of a set…
condition: "input.priority in ['high', 'urgent']"
# …or when a numeric threshold is crossed
condition: "input.amount * input.quantity >= 1000"
Condition literals
Trigger conditions use the JSON-style literals true, false, and null. This is a deliberately narrow expression language — there is no way to call functions or reach outside the payload, so an inbound webhook body can never execute code.
Cron triggers¶
A cron trigger runs its automation on a schedule. Set type: cron and give a crontab expression in cron:
# triggers/nightly_cleanup.yaml
name: nightly_cleanup
label: Nightly cleanup
type: cron
cron: "0 2 * * *" # every day at 02:00
automation: cleanup
The cron value is standard crontab (minute hour day-of-month month day-of-week):
| Expression | Meaning |
|---|---|
"0 9 * * *" |
Every day at 09:00 |
"*/15 * * * *" |
Every 15 minutes |
"0 9 * * 1" |
Every Monday at 09:00 |
"0 0 1 * *" |
Midnight on the 1st of each month |
When the schedule elapses, the trigger fires the automation with an empty input ({}). An invalid crontab expression fails the module install, so a scheduling typo can't slip into a running instance.
To change a schedule, edit the cron value in the YAML and reinstall the module. To stop a scheduled trigger, set enabled: false and reinstall.
Webhook triggers¶
A webhook trigger fires when an external system sends a POST request. Set type: webhook — no cron is needed:
# triggers/ticket_hook.yaml
name: ticket_hook
label: Inbound ticket webhook
type: webhook
automation: create_ticket
The endpoint¶
On install, the trigger is exposed at a stable URL and a secret is generated for it:
POST /api/hooks/{name}
X-Webhook-Secret: <secret>
Content-Type: application/json
{ ...automation input... }
{name}is the trigger'sname(here,ticket_hook).- The request body becomes the automation's
input— so your automation and anyconditionread fields straight off it. - The
X-Webhook-Secretheader must match the trigger's generated secret; requests without it (or with the wrong value) are rejected.
The request body is always treated as data, never executed.
Finding the secret¶
The secret is created when the module installs. Open the Triggers view in the app (it lives under the base app's menu), open your webhook trigger, and copy the secret from its configuration. Configure the sending system with that value in the X-Webhook-Secret header.
Reinstalling regenerates the secret
A fresh secret is generated every time the module is installed — including every reinstall or upgrade. Because changing a cron schedule or disabling a trigger both require reinstalling the module, reshipping a module that owns a webhook trigger invalidates the previously distributed X-Webhook-Secret: external callers using the old value are silently rejected until you copy the new secret from the Triggers view and reconfigure them. Plan to redistribute the secret whenever you reinstall such a module.
Calling it¶
curl -X POST https://your-instance.example.com/api/hooks/ticket_hook \
-H "X-Webhook-Secret: $SECRET" \
-H "Content-Type: application/json" \
-d '{"subject": "Cannot log in", "priority": "high", "customer_id": 42}'
The response reports whether the automation fired and its outcome. A disabled trigger, an unknown name, or a bad secret is rejected without running anything.
Manual triggers¶
A manual trigger has neither a schedule nor a webhook endpoint. Set type: manual and name the automation it binds to:
# triggers/escalate_now.yaml
name: escalate_now
label: Escalate now (manual)
type: manual
automation: escalate
Unlike cron and webhook triggers, a manual trigger is not fired automatically: the schedule never runs it, and the POST /api/hooks/{name} endpoint serves webhook triggers only (it rejects any other type). It still installs and appears in the Triggers view like any other trigger, recording the binding between a name and its automation.
What happens when a trigger fires¶
When a trigger fires, it follows the same path:
- Condition gate — if a
conditionis set and evaluates falsy (or errors), stop here. The automation does not run and the trigger is not marked fired. - Run the automation — the named automation runs with the trigger's input (the webhook body, or
{}for cron). - Record the run — the trigger stamps its last-fired time, and the automation records its own run outcome (
done,input_rejected,exhausted, …).
Each automation run is observable after the fact — see Automations for how runs are recorded and inspected.
Managing triggers in the app¶
Installed triggers show up in the Triggers view, rendered by the generic list/form framework like any other model. From there you can see each trigger's type, the automation it targets, whether it's enabled, and (for webhooks) its secret. Because triggers are ordinary module-owned records, uninstalling the module removes its triggers automatically.
Checklist¶
- [ ] Trigger YAML lives under
triggers/and is listed inmodule.yaml'striggers:key. - [ ]
typeiscron,webhook, ormanual. - [ ]
cronis set (and valid) for cron triggers. - [ ]
automationnames an automation defined in this module or one itrequires. - [ ] Any
conditionreads only frominputand uses the allowed operators.
Related¶
- Automations — the action a trigger runs.
- Workflows — longer stateful processes an automation can drive.
- Models & fields — the data your automations read and write.
- Module structure & manifest — where
triggers:fits inmodule.yaml.