Besides POST /v1/routines/{routine}/runs, a routine starts runs from its triggers, up to 20 of them. Each trigger has an id, unique in the routine. Triggers switch atomically when you publish a new version, and pause together when you pause the routine.

Every trigger can take with, which builds the run's input from the trigger context (absent, the input is {} plus the input defaults), and if, a condition that must hold for a run to start.

Cron

yaml
triggers: - type: cron id: nightly cron: "0 30 6 * * MON-FRI" # 06:30:00 on weekdays tz: America/Los_Angeles catch_up: latest overlap: skip
  • cron has six fields, seconds first: second, minute, hour, day of month, month, day of week. Numbers, names (JAN, MON), *, ranges, lists, steps (*/5) and ? are accepted, plus L, W and # in the day fields.
  • tz is an IANA time zone, UTC by default. A local time skipped by a daylight-saving change fires at the first instant after the gap; a local time that occurs twice fires once.
  • catch_up is required. It decides what happens to occurrences missed while the node was down or the routine was paused, within catch_up_window_s (a day by default): skip runs none, latest runs only the newest, all runs each in order.
  • overlap decides what happens when an occurrence fires while this trigger's previous run is still active: skip (the default) drops it, buffer_one keeps one pending occurrence, allow starts it anyway, cancel_running cancels the active run first.

Occurrences are scheduled ahead and deduplicated, so one occurrence never starts two runs. The target for firing lateness is p99 5 ms; see benchmarks.

Interval

yaml
- type: interval id: every_five_minutes every_ms: 300000 anchor: epoch catch_up: skip

every_ms is at least 1000. anchor: epoch (the default) fires on multiples of the period since the Unix epoch, aligned to the clock; publish fires one period after the version became active, then every period. catch_up and overlap work as for cron.

Webhooks

yaml
- type: webhook id: inbound verify: scheme: b3os with: pr: ${{ trigger.body.number }}

Publishing gives the trigger a hook with an unguessable ID and the URL to hand to the sender:

text
https://beta-api.b3os.org/v1/routines/hooks/hk_01m5t3r2qwz23751t0x74bhahz

The routine's triggers[].hook shows the URL. Every delivery must pass verification; there is no unverified webhook. verify.scheme is one of:

SchemeVerifies
b3osB3OS-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, with a secret B3OS generates. The secret is returned once, in the publish response.
github, stripe, slack, standard_webhooksThe provider's own signature scheme
hmac_sha256A generic HMAC over <timestamp>.<body>: needs header, timestamp_header and a dedupe key on the trigger, and optionally encoding and prefix

For every scheme except b3os, store the provider's signing secret as a verification-only vault connection (type api_key, no pack and no hosts, so it is never injected anywhere) and name it in verify.secret. Timestamped schemes refuse deliveries more than tolerance_s from the node's clock (300 seconds, the most allowed). A bad signature is webhook_signature_invalid.

A delivery whose if is false is acknowledged with 202 and starts nothing. If the node is down, the edge stores deliveries durably and replays them in order when it returns. In expressions, trigger holds the request: hook_id, delivery_id, received_at, method, headers (lowercase), query and body (parsed when it is JSON).

Duplicate deliveries

Senders retry, so a webhook trigger drops repeats by a key. dedupe sets that key as one expression over trigger and vars, such as ${{ trigger.headers['x-delivery-id'] }}. A second delivery with the same key within 24 hours gets the first delivery's response and starts nothing. Without dedupe, each scheme uses its own key:

SchemeDefault key
githubThe X-GitHub-Delivery header
stripeThe event's id in the body
slackThe event_id in the body
standard_webhooksThe webhook-id header
b3osThe Idempotency-Key header if the sender sends one, otherwise the v1 signature
hmac_sha256None: dedupe is required, and a definition without it does not publish
yaml
- type: webhook id: orders verify: scheme: hmac_sha256 secret: conn_01m5t6cbgdjs1ketgcqwa2sne8 header: X-Shop-Signature timestamp_header: X-Shop-Timestamp dedupe: ${{ trigger.headers['x-shop-delivery'] }}

Send a test delivery

A b3os hook needs no API key: the hook ID and the signature authenticate the delivery. To try one, sign a body with the secret from the publish response and post it to the hook URL:

bash
BODY='{"number": 42}'T=$(date +%s)SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$HOOK_SECRET" | sed 's/^.* //')curl -s "$HOOK_URL" \ -H "Content-Type: application/json" \ -H "B3OS-Signature: t=$T,v1=$SIG" \ -d "$BODY"

The answer is 202 with a hook_delivery: its run_id, or deduplicated: true for a repeat. A delivery that fails verification gets webhook_signature_invalid.

Rotate the secret

POST /v1/routines/{routine}/hooks/{hook_id}/rotate issues a new b3os secret and keeps the previous one valid for grace_s (a day by default) so the sender can switch. Hook IDs stay the same across versions for the same trigger ID.

Pack triggers

A trigger of type: action subscribes the routine to a trigger a marketplace pack provides, such as an issue being opened or an invoice being paid:

yaml
- type: action id: invoice_paid uses: io.b3os/stripe/event@1 with: types: [invoice.paid] connections: secret_key: conn_01m5t6cbgdjs1ketgcqwa2sne8 if: trigger.event_type == 'invoice.paid'
  • uses names the pack trigger; a major version such as @1 resolves to the newest matching release when you publish and is pinned there.
  • with configures the trigger and is evaluated once, at publish, with vars only. connections binds the trigger's connection keys.
  • Publishing creates or updates one trigger subscription (tsub_) per action trigger; a new version without the trigger, a rollback to a version without it, or deleting the routine removes it. Subscriptions are read-only at GET /v1/actions/subscriptions.
  • Each event that passes if starts a run as the publishing key, with the event in trigger.data. Delivery is at least once, and a repeated event ID starts nothing.

GET /v1/actions/triggers lists the triggers packs provide, with their configuration and event schemas.

When triggers stop

Pausing the routine (PATCH with {"paused": true}) stops every trigger until you resume it. Revoking or expiring the key that published the active version pauses the triggers too, until you publish a new version with a valid key; pack trigger subscriptions show paused meanwhile and drop events.

Ask a question... ⌘I