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 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 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 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.
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 uses its triggers differently: a matching event starts or resumes a session, and the session runs turns. See 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.
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 useuuid(). - 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.
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. 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.
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 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
responseTimeoutso a healthy run is never cut off. The tutorial deliberately sets it below the handler's sleep to demonstrate retries. - Rate-limited downstreams. Raise
retryDelayand lowerretryBackofftoward1for a flat, predictable cadence. - Time-sensitive work. Lower
timeoutso stale work is exhausted rather than delivered late, and treatEXHAUSTEDexecutions as an alert. - Asynchronous handlers.
completionTimeoutbounds 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 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 and 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.
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.
- 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 theexecutionIdyou supply names the new execution and makes the request idempotent. See 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
CANCELLEDwhen the invocation ends. Work already sent to another system is not undone. See 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 to run a triggered action end to end, then Building Transactional Actions for a Starlark program with atomic writes and safe reruns.
- Read Tools to call the same kind of program directly.
- Read the Actions reference and the Executions reference for every field, operation, status, and limit.