Triggers
Most jobs run on a schedule or after a parent job. A trigger job is different: it fires
on an external event — an inbound HTTP call, a new file, new mail, an upcoming calendar event, a
Telegram message, or a new spreadsheet row. The event’s data flows into the run (and any jobs
chained after it) as $parent.
There are two kinds:
- Inbound —
webhook: something outside Cronable calls it. The event reaches your daemon through Cronable’s hosted relay, so your server never opens a port. - Polling —
folder-watch,inbox,calendar-watch,telegram-watch,sheet-watch,hostaway-watch: Cronable checks a source on an interval and fires when something new appears.
webhook — fire on an inbound HTTP call
A webhook job fires when an external system POSTs to it. Because the daemon binds to 127.0.0.1
and never opens an inbound port, the public endpoint lives on Cronable’s relay at
hooks.cronable.ai, and your daemon reaches it outbound:
- Your daemon opens and holds an outbound connection to the relay — nothing to expose, and it works behind NAT or a firewall.
- The relay gives the hook a public URL,
https://hooks.cronable.ai/<hookId>, wherehookIdis an unguessable, URL-safe path component (the relay requires at least 32 characters — generated hookIds already are). - A third party POSTs there; the relay pushes the event down your existing connection and the job runs. The event passes through in transit and is not stored.
Auth is a per-hook secret token — generate or rotate it in the job’s Inspector (the Webhook
section), and have the caller present it as the X-Webhook-Token header or a ?token=… query
param. It’s verified in constant time against a stored hash; an unknown hook, a disabled hook, or a
missing/bad token all return the same generic 401 (a caller can’t probe which hooks exist).
The request becomes the run’s output, so a job chained after it reads:
{{ $parent.json.* }}— the body parsed as JSON,{{ $parent.text }}— the raw body,{{ $parent.query.* }}— inbound query params.
# jobs/intake/on-order.yaml — the trigger
id: on-order
name: On order webhook
group: intake
type: webhook
hookId: 3f9c1b0a7e5d4c2f8a6b9e0d1c2f3a4b # the public path component on hooks.cronable.aiThen wire a child with an afterParent schedule point to process {{ $parent.json.* }}.
Full detail — how to enable it, what data crosses, and the security model that keeps your hooks yours: Webhook relay.
Advanced — self-hosted inbound. Prefer not to use the relay? Put the daemon behind your own authenticated reverse proxy and route
POST /engine/api/hooks/<hookId>to it — you then own the TLS, the public exposure and the hardening. See Security.
Polling triggers
Each polling trigger checks its source every pollSeconds and fires one run per NEW item.
On the first poll it baselines the source (existing items are recorded, never replayed), so a
new job never floods you with history; from then on only genuinely new items fire. Delivery is
at-most-once (a crash mid-fire loses that one item rather than risking a re-fire storm).
A hung or unreachable source can’t wedge the daemon. Each poll runs under a per-tick timeout: if
the source stops responding — a dead IMAP server, an unreachable API, a flaky network mount — the
poll is aborted at the timeout instead of blocking forever, so your other jobs keep running. Repeated
failures back off automatically and show up in the run history (a poller.error), and polling resumes
the moment the source is healthy again (poller.recovered). A failed poll fires nothing, and because a
source is baselined on its first successful poll, a transient outage never replays history when it
recovers.
folder-watch — a file lands in a directory
| Field | What it is |
|---|---|
path | the directory to watch |
pattern | optional glob/name filter (e.g. *.csv) |
pollSeconds | optional check interval (default 30, floor 5) |
inbox — new IMAP mail
Fires one run per new message. credential is an imap credential (username + password — e.g. a
Gmail app password), plus host and optional port (default 993) / mailbox (default INBOX)
/ pollSeconds (suggest ≥ 60). The message envelope + body arrive in $parent.
calendar-watch — a Google Calendar event is about to start
credential (a google_service_account), calendarId, and optional leadMinutes (default 15) /
pollSeconds (default 30 — but a longer interval is fine here; calendar events rarely change
minute-to-minute). Fires once per event when it’s within leadMinutes of starting — handy
for reminders or pre-meeting automations.
telegram-watch — a new Telegram bot message
credential (a Telegram bot token), optional chatId (restrict to one chat), command (fire only
when the message text starts with it, e.g. /run), and pollSeconds.
sheet-watch — a row is appended to a Google Sheet
credential (a google_service_account), spreadsheetId, and range (A1 notation of the watched
tab, e.g. Sheet1!A:Z), plus optional pollSeconds (default 30). Fires when a new row appears.
hostaway-watch — a new short-term-rental reservation is booked
credential (a basic credential — username = your Hostaway Account ID, password = your API
key), optional listingId (watch a single property; blank = all listings), and optional
pollSeconds (default 300 — reservations change slowly, so 5 minutes is plenty; floor 5). Detection
is by reservation id: the first poll records the current newest id and fires nothing, so pointing
a new job at an account with existing reservations never replays them. The whole reservation arrives
in $parent ({{ $parent.json.guestEmail }}, {{ $parent.json.arrivalDate }}, …) — chain a job
after it to text the guest a door code, add a cleaning task, or message them in Hostaway. See the
short-term-rental recipe.
The event data flows to the next job
A trigger job’s output is the event payload. Chain a job after it (an afterParent point with
on: succeeded) and read the data through $parent — see the
Expressions & data flow guide. Managed connections for the SaaS pollers
(inbox, calendar-watch, telegram-watch, sheet-watch) are stored as reusable credentials
under Settings → Secrets → Credentials — see Security.