# Triggers and Executions

Run durable, retried work when a record in your ledger changes or on a schedule, with a full history of every invocation.

A **trigger** decides when Twisp runs an action on its own. An event trigger is a [CEL](https://www.twisp.com/docs/reference/cel.md) expression over database change data capture (CDC) events: every insert, update, and delete in Twisp produces an event, and an action runs for each one its triggers match. A schedule trigger runs the action at set times instead. Triggers are one way to run an action; a tool can also be called or started by a client, an agent, or another tool. See [Actions](https://www.twisp.com/docs/actions.md) for the whole model.

Each run is an **execution**: a durable record that tracks every invocation, retries on failure, keeps the event and the output, and can be rerun or cancelled through the API. An **invocation** is one call to the action's handler within an execution. A retry starts another invocation in the same execution. If a handler accepts work asynchronously, that invocation remains open until it completes or reaches its deadline.

Use a triggered action when something has to happen after a ledger change, or at a set time, and you need to know that it happened.

## Triggered actions and webhooks

Both features start from the same events and the same CEL filters. They differ in who runs the work and what Twisp remembers about it.

|                             | Webhook                                 | Triggered action                                                                                 |
| --------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------ |
| What happens on a match     | Twisp sends an HTTP `POST` to your URL. | Twisp runs the handler the action defines.                                                       |
| Retries                     | Delivery retries only.                  | Handler retries with configurable backoff, a total time budget, and a per-invocation deadline.   |
| Record of the work          | None beyond delivery.                   | An execution with status, every invocation, the exact typed event, the output, and full history. |
| Operator control            | Enable or disable the endpoint.         | Inspect executions, rerun one against the same event, or cancel queued work.                     |
| Reads its own settings from | The endpoint at delivery time.          | The action version that was active when the event occurred.                                      |

A webhook is the right choice when you want a notification and your own system owns the follow-up. A triggered action is the right choice when the follow-up itself should be durable, observable, and replayable. See the [Webhooks reference](https://www.twisp.com/docs/reference/api/webhooks.md) for the notification path.

## The lifecycle

A triggered action moves through three stages, and each stage leaves records you can query.

### 1. Define and publish

You create an action with a handler `kind`, its configuration, policies, and execution settings, and give it one or more triggers. Defaults fill in anything you omit. Validate first with `validateAction`, which resolves defaults and evaluates the triggers against sample events without saving anything.

Changes take effect asynchronously. A new or edited action reports `status: UNPUBLISHED` or its previous published status, with `pending` naming the state it is waiting to reach. Once published, `pending` clears and `published` records the moment. Events that occur before that moment use the previously published definition, if any. See [Publication status](https://www.twisp.com/docs/reference/actions/actions.md#publication-status).

> **Note:**
>
> Creating an action does not replay earlier events. Wait for `status: ACTIVE` before producing the events you want it to handle.

### 2. Match

Each committed change is evaluated against every active action's event triggers. A match creates one execution per action, even when several of the action's triggers match, pinned to the exact action version that was active at the event's commit time and to a copy of the event. The execution's `matchedTriggers` records which triggers matched. Editing the action later does not change existing executions.

An action with a [session](https://www.twisp.com/docs/actions/sessions.md) uses its triggers differently: a matching event starts or resumes a session, and the session runs turns. See [Session roles](https://www.twisp.com/docs/reference/actions/sessions.md#session-roles).

### 3. Execute

An execution starts `QUEUED`. Each invocation moves it to `RUNNING`, and the handler must answer within the response timeout. A handler can succeed, fail, or accept the work and finish asynchronously, in which case the execution is `ACCEPTED` until the handler reports its outcome.

A retryable failure schedules the next invocation with backoff and sets `nextInvocationAt`. Work stops when it succeeds, when a failure is not retryable, or when the invocation or time budget is exhausted. Every transition is kept in the execution's history, so you can read what each invocation saw and returned.

A triggered execution runs as the action's own principal, under the action's `policies`. A call to a tool runs as its caller instead. See [Principal and policies](https://www.twisp.com/docs/reference/actions/actions.md#principal-and-policies).

## Designing triggers

Each trigger's `event` is a boolean CEL expression over `event`, the complete event. An action can have up to 16 triggers; an event matching any of them runs the action. Use separate triggers for unrelated conditions, such as journal updates and large balance changes, rather than one long `||` expression: each is validated separately, and the execution records which one matched. Give a trigger a `name` when something else refers to it, such as a session's turn rules.

`event.type` names the event type, `event.data` holds the record after the change, and `event.previous` the record before it; the remaining keys are snake_case, such as `event.record_id`, and so are the record fields inside `data`: `journal_id`, `dr_balance`. Every field keeps its type. A UUID is a UUID, money is money, and a KV record's `value` is the same typed document you wrote, so you compare with the constructors you already use elsewhere in Twisp instead of parsing strings.

Some patterns that come up often:

- **One record type and operation.** `event.type == "journal.updated"`.
- **A specific record.** `event.data.journal_id == uuid("e3a9...")`. A typed field never equals a plain string, and a trigger that compares one with a string is rejected, so always use `uuid()`.
- **A change in a field.** `event.previous != null && event.previous.description != event.data.description`.
- **A balance threshold.** `event.data.settled.dr_balance > money("1000", "USD")`. Money compares as money; `money.Sub(event.data.settled.dr_balance, event.previous.settled.dr_balance)` gives the change.
- **A typed KV value.** `event.data.value.expires < date("2027-01-01") && event.data.value.count > 9007199254740992u`. Dates, timestamps, durations, decimals, and money order with `<`, `<=`, `>`, and `>=`, and integers keep their full 64-bit range.
- **Missing fields.** `event.?previous.description.orValue("") == ""` supplies a default instead of failing on an absent field.
- **Changes to actions records.** Executions, sessions, and agent memory produce events too, such as `action_execution.updated`, so one action can follow another's work. Such chains stop before they repeat an action; see [Loops](https://www.twisp.com/docs/reference/actions/executions.md#loops).

A trigger that errors for an event, for example by reading a missing field, does not match that event and records a diagnostic; it never blocks the action's other triggers or other actions. A field that the record of the trigger's event type never has, such as `event.data.settled.normal_balance` or `event.data.accountId`, is rejected when you create, update, or validate the action; see [Triggers](https://www.twisp.com/docs/reference/actions/actions.md#triggers). `validateAction` returns the error text per sample, so test edge cases such as inserts, where `previous` is null. Samples are CEL maps, so write typed fields the same way: `{"type": "journal.updated", "data": {"journal_id": uuid("e3a9...")}}`.

## Schedule triggers

A trigger with a `schedule` in place of `event` runs the action at set times, for work that no record change starts, such as a nightly reconciliation. A schedule trigger requires a `name`, and its `cron` is a standard 5-field expression evaluated in an IANA `timezone`, UTC when omitted.

```graphql
triggers: [
  {
    name: "nightly"
    schedule: { cron: "0 2 * * *", timezone: "America/New_York" }
  }
]
```

Each firing is an event of type `schedule.fired` whose `data` holds the trigger's name and the scheduled time. A firing reaches only its own trigger, fires within about a minute of its time, and is delivered once even if it arrives twice. For an action without a session, each firing runs the action once; for a session action, a firing can start a session or resume one by ID. Schedules belong to the action's published version: publishing a new version starts, changes, or stops them, and a disabled or deleted action does not fire. A deployment without a scheduler rejects schedule triggers. See [Schedule triggers](https://www.twisp.com/docs/reference/actions/actions.md#schedule-triggers) for the cron rules, daylight-saving behavior, and session roles.

## Choosing execution settings

The defaults suit a handler that is usually fast and occasionally flaky: a one minute response timeout, retries starting at one minute and doubling to an hourly ceiling, twenty-four invocations, and a twenty-four hour budget for the whole execution.

Tune them when your handler differs:

- **Slow handlers.** Raise `responseTimeout` so a healthy run is never cut off. The [tutorial](https://www.twisp.com/docs/tutorials/actions.md) deliberately sets it below the handler's sleep to demonstrate retries.
- **Rate-limited downstreams.** Raise `retryDelay` and lower `retryBackoff` toward `1` for a flat, predictable cadence.
- **Time-sensitive work.** Lower `timeout` so stale work is exhausted rather than delivered late, and treat `EXHAUSTED` executions as an alert.
- **Asynchronous handlers.** `completionTimeout` bounds how long an accepted invocation may take to report back.

Twisp rejects combinations that could never take effect, such as a `retryDelay` longer than `timeout`. [Execution settings](https://www.twisp.com/docs/reference/actions/actions.md#execution-settings) lists every setting, its default, and its limit.

## Executions as records

Every run of every action is an execution, whatever started it: a matching event, a schedule, a client's call or start, or another execution's call. Executions are records like any other in Twisp, with versions and history, so they answer what ran, on what input, under which version, and what came of it.

| Field             | What it records                                                                                                                                                      |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`          | Where the execution is: `QUEUED`, `RUNNING`, `ACCEPTED`, or `RETRY_SCHEDULED` while it runs; `SUCCEEDED`, `FAILED`, `EXHAUSTED`, or `CANCELLED` once it is terminal. |
| `actionVersion`   | The exact action version it runs. It never changes.                                                                                                                  |
| `event`           | What it received: the typed event for a trigger, or an `action.invoked` event whose `data` is the input for a call.                                                  |
| `matchedTriggers` | Which triggers matched.                                                                                                                                              |
| `invocation`      | The current invocation: its number, deadlines, `output` or `error`, and the tool and skill versions it resolved.                                                     |
| `history`         | Every earlier version of the record, newest first. Each status change writes a version, so earlier invocations are read here.                                        |

A terminal execution never changes status again. A failed invocation's `error` carries a `code`, a `message`, whether it was `retryable`, and often a `hint` saying what to change. See [ActionExecution](https://www.twisp.com/docs/reference/actions/executions.md#action-execution) and [Execution status](https://www.twisp.com/docs/reference/actions/executions.md#execution-status).

Executions also link to one another. A call made by a tool or an agent is a child execution of its caller, `rerunOf` names the source of a rerun, and `startedBy` names how a started execution began. `lineage` walks the whole tree of executions that one caused, or that caused it, so an investigation can follow any piece of work to everything it touched. See [Lineage](https://www.twisp.com/docs/reference/actions/executions.md#lineage).

## Operating executions

List executions by action, event, status, or creation time in either order, read an exact historical version, and page through history. See [Listing and filtering](https://www.twisp.com/docs/reference/actions/executions.md#listing-and-filtering).

- **Rerun** a terminal execution to run the same event again, either with its pinned action version or with another exact version. A rerun is a new execution linked to its source by `rerunOf`, and the `executionId` you supply names the new execution and makes the request idempotent. See [Manual reruns](https://www.twisp.com/docs/reference/actions/executions.md#manual-reruns).
- **Cancel** an execution that has not finished. Queued and retry-scheduled work is cancelled at once. A running invocation is stopped instead: its writes roll back and the execution becomes `CANCELLED` when the invocation ends. Work already sent to another system is not undone. See [Cancellation](https://www.twisp.com/docs/reference/actions/executions.md#cancellation).
- **Disable or delete** the action to stop new matches and schedule firings. Existing executions continue, so cancel them separately if they should not.

Handlers can run more than once for the same event, for example after a retry whose first invocation actually succeeded but failed to report. Make the work idempotent, keyed on `executionId` or the event's `eventId`.

## Next steps

- Follow [Running Actions Locally](https://www.twisp.com/docs/tutorials/actions.md) to run a triggered action end to end, then [Building Transactional Actions](https://www.twisp.com/docs/tutorials/advanced-actions.md) for a Starlark program with atomic writes and safe reruns.
- Read [Tools](https://www.twisp.com/docs/actions/tools.md) to call the same kind of program directly.
- Read the [Actions reference](https://www.twisp.com/docs/reference/actions/actions.md) and the [Executions reference](https://www.twisp.com/docs/reference/actions/executions.md) for every field, operation, status, and limit.
