# Triggers (/en/docs/developers/automation/guides/triggers)



Triggers separate when work runs from its definition.

Before running this page, complete quickstart steps 1–3 in the same Bash session: [set `PRAXIS_API_URL` and `PRAXIS_API_KEY`](/en/docs/developers/automation/quickstart#environment), [define `SOURCE` and run it](/en/docs/developers/automation/quickstart#run-source), then [define and run `read_result`](/en/docs/developers/automation/quickstart#read-result). The helper checks for answer 42, as expected by the arithmetic examples here. Also [save and publish the script](/en/docs/developers/automation/quickstart#publish-script) and [set `TARGET`](/en/docs/developers/automation/quickstart#run-version) in steps 4–5. `TARGET` must reference that published script, which accepts `n` and returns `answer`.

## Scheduled execution [#scheduled-execution]

Choose exactly one schedule form:

| Form                | Use                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------- |
| `cron: "0 9 * * *"` | Daily at 09:00 local time; five fields, minute precision, with L, W and # support.  |
| `every: "PT5M"`     | Every five elapsed minutes; the minimum is one minute.                              |
| `at`                | An absolute RFC 3339 timestamp with offset or Z, between now and one year from now. |

`timezone` accepts an IANA name and defaults to UTC. For local cron schedules, nonexistent spring DST times are skipped and repeated autumn times execute once. `every` uses elapsed time and `at` uses an absolute instant; neither uses the cron DST rule.

[Preview the next occurrences](/en/docs/api/native/automation/previewTriggerSchedule):

```bash
curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers/preview" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"schedule":{"cron":"0 9 * * *"},"timezone":"Asia/Shanghai"}' \
  | jq -e '{next_fire_times} | select(.next_fire_times | length == 5)'
```

Expected: five future timestamps, for example:

```json
{
  "next_fire_times": [
    "2026-10-13T01:00:00.000Z",
    "2026-10-14T01:00:00.000Z",
    "2026-10-15T01:00:00.000Z",
    "2026-10-16T01:00:00.000Z",
    "2026-10-17T01:00:00.000Z"
  ]
}
```

Dates depend on when you preview. `Z` means UTC. For example, `2026-10-13T01:00:00.000Z` is &#x2A;*October 13 at 09:00 in Shanghai (UTC+8)**. The `timezone` controls the cron schedule; returned timestamps are always expressed in UTC.

[Create the trigger](/en/docs/api/native/automation/createTrigger), then fire it once to check the target:

```bash
TRIGGER=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" '{
    name:"daily-hello",target:$target,kind:"schedule",input:{n:41},
    schedule:{cron:"0 9 * * *"},timezone:"Asia/Shanghai",
    overlap:"skip",catchup_window_seconds:300
  }')")
SCHEDULE_ID=$(jq -er '.id' <<< "$TRIGGER")
jq -e '.state == "enabled" and (.next_fire_times | length == 5)' <<< "$TRIGGER"
FIRING=$(curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/fire" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}')
jq -e '.outcome == "started"' <<< "$FIRING"
read_result "$(jq -er '.run_id' <<< "$FIRING")"
```

Expected: all three assertions print `true`. `started` confirms launch; `read_result` uses the returned `run_id` to [retrieve the run](/en/docs/api/native/automation/getRun), check completion and confirm answer 42.

Pin `target` with `@1.0.0` or follow a tag with `@latest`. Every firing records the resolved version. Static input is validated on creation and update, then checked again when firing in case the target's schema changed.

## Overlap, delays and active windows [#overlap-delays-and-active-windows]

The default `overlap: "skip"` records `skipped_overlap` if the preceding run is unfinished. `allow` starts overlapping runs subject to concurrency limits. Buffering, cancelling previous runs and historical backfill are not available.

`catchup_window_seconds` defaults to 300. A scheduling delay beyond this window produces `missed` without starting a run. This does not promise to replay every occurrence after resuming. Paused triggers do not execute; resuming continues future scheduling.

Set RFC 3339 `start_at` and `end_at` timestamps to bound the active window. A one-shot `at` expires after firing; reaching the end time also makes the trigger `expired`. 5 consecutive firing failures pause it automatically. Use [pause](/en/docs/api/native/automation/pauseTrigger) and [resume](/en/docs/api/native/automation/resumeTrigger) to control future firings.

Pause the trigger created above, then resume it:

```bash
curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/pause" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}' \
  | jq -e '.state == "paused" and (.next_fire_times | length == 0)'
curl --fail-with-body -sS -X POST \
  "$PRAXIS_API_URL/automation/v1/triggers/$SCHEDULE_ID/resume" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' --data '{}' \
  | jq -e '.state == "enabled" and (.next_fire_times | length == 5)'
```

Expected: both requests print `true`. Resuming this daily schedule returns its next five occurrences.

A workflow YAML cron/every schedule manages a `source=definition` trigger when published as latest. Change it by republishing the definition, not by PATCHing the trigger. You can pause or resume it. `schedule.after` and `schedule.on` are not supported yet.

## Webhooks [#webhooks]

With `input_mode: "body"`, incoming JSON becomes run input; non-JSON bodies become strings. This sends `n:41` to the quickstart script:

```bash
HOOK=$(curl --fail-with-body -sS "$PRAXIS_API_URL/automation/v1/triggers" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data "$(jq -n --arg target "$TARGET" '{
    name:"webhook-hello",target:$target,kind:"webhook",webhook:{input_mode:"body"}
  }')")
WEBHOOK_ID=$(jq -er '.id' <<< "$HOOK")
WEBHOOK_URL=$(jq -er '.webhook_url' <<< "$HOOK")
curl --fail-with-body -sS "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H "webhook-id: hello-$(date +%s)-$RANDOM" \
  --data '{"n":41}' | jq -e '.id | startswith("frg_")'
```

Expected: HTTP 202 and `true`, with a firing ID in the response. Check firing history for the asynchronous launch result.

The full URL is returned only on creation or [rotation](/en/docs/api/native/automation/rotateTriggerWebhook). Its token is the credential: no API Key is needed, and the URL must stay private. Rotation invalidates the old URL. Invalid tokens, paused triggers and nonexistent triggers return 404. Bodies are limited to 1 MiB. The same `webhook-id` is deduplicated for 24 hours.

## Signature headers and envelopes [#signature-headers-and-envelopes]

The endpoint authenticates the secret URL. It does not automatically verify Standard Webhooks, GitHub or Stripe HMAC signatures. Supplying `webhook-signature` does not mean it was verified.

To read headers, create the trigger with `webhook: {input_mode:"envelope"}`. The target schema must accept the following shape rather than the quickstart's `n` input:

```json
{
  "body": { "n": 41 },
  "headers": { "content-type": "application/json", "webhook-id": "delivery-1" },
  "webhook_id": "delivery-1",
  "received_at": "2026-10-11T12:00:00.000Z"
}
```

An absent `webhook_id` becomes null. Only these headers are forwarded: `content-type`, `user-agent`, `webhook-id`, `webhook-timestamp`, `webhook-signature`, `x-github-event`, `x-github-delivery`, `x-hub-signature-256` and `stripe-signature`. Authorization and Cookie are excluded.

JSON bodies are parsed before delivery; the envelope has no separate original-byte field. For protocols that sign raw bytes, `JSON.stringify(body)` cannot substitute for the original payload. Verify such signatures in your receiving service before forwarding to the secret URL. There is currently no generic verification example that can safely be applied directly to this JSON envelope.

## Inspect firings and clean up [#inspect-firings-and-clean-up]

```bash
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/triggers/$WEBHOOK_ID/firings" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  | jq -e '.data | length > 0'
curl --fail-with-body -sS \
  "$PRAXIS_API_URL/automation/v1/triggers/$WEBHOOK_ID/firings" \
  -H "Authorization: Bearer $PRAXIS_API_KEY" \
  | jq '.data[] | {outcome,run_id,resolved_version,error}'
```

Expected: the first request prints `true`; the second lists records. Read again shortly if asynchronous processing is still underway. `started` means launched; inspect error for `failed`. `missed` exceeds the catchup window, `skipped_overlap` has an unfinished prior run, and `deduped` matched a prior delivery. Follow `run_id` to inspect terminal execution status. Firing records are retained for 30 days.

Delete both tutorial triggers when finished to stop future scheduled execution:

```bash
for id in "$SCHEDULE_ID" "$WEBHOOK_ID"; do
  curl --fail-with-body -sS -X DELETE \
    "$PRAXIS_API_URL/automation/v1/triggers/$id" \
    -H "Authorization: Bearer $PRAXIS_API_KEY"
done
```

Expected: both curl requests exit 0. Deleting a trigger does not affect runs already started.

[API reference →](/en/docs/api/native/automation) · [Cookbook →](/en/docs/cookbook)
