Skip to content

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']
# Only escalate tickets whose priority came in as "high"
condition: "input.priority == 'high'"
# 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's name (here, ticket_hook).
  • The request body becomes the automation's input — so your automation and any condition read fields straight off it.
  • The X-Webhook-Secret header 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:

  1. Condition gate — if a condition is set and evaluates falsy (or errors), stop here. The automation does not run and the trigger is not marked fired.
  2. Run the automation — the named automation runs with the trigger's input (the webhook body, or {} for cron).
  3. 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 in module.yaml's triggers: key.
  • [ ] type is cron, webhook, or manual.
  • [ ] cron is set (and valid) for cron triggers.
  • [ ] automation names an automation defined in this module or one it requires.
  • [ ] Any condition reads only from input and uses the allowed operators.