Skip to content

Automatic workflow triggers ​

A published workflow can start by itself: a run per ingested event of the types it names (trigger event), or per new subject (trigger subject.created). The integrating system sends events (POST /events) and creates subjects (POST /subjects, POST /decide) as it does anyway, and the runs follow, without a POST /workflow-runs per event. A filter narrows what starts a run, a debounce window keeps a subject's burst of events to two runs, and a rate cap bounds the runs a minute.

Arming a trigger ​

A workflow's trigger starts runs once the workflow is published with a triggerConfig:

bash
curl -X PUT "$API/workflow-definitions/card-monitoring" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Card monitoring",
    "trigger": "event",
    "triggerConfig": {
      "eventTypes": ["payment"],
      "filter": "event.amount > 1000.0 && event.attributes.status == \"approved\"",
      "debounceSeconds": 300,
      "maxRunsPerMinute": 600
    },
    "definition": { "entryStepId": "rules", "steps": [ … ] }
  }'
FieldWhat it does
eventTypesThe event types that start a run, up to 20. Required for event; subject.created takes none.
subjectKindsOnly subjects of these kinds start a run (user, merchant, seller); any when unset.
filterA CEL expression over event, subject and now that must be true, up to 2 000 characters; checked when saved.
debounceSeconds0 to 86 400 (default 0). Within this long of a subject's last start, its matches wait for the window's end, where the newest of them starts one more run and the others are coalesced into it.
maxRunsPerMinute1 to 6 000, across subjects. Starts over the cap wait until the oldest start of the last minute is a minute old; none is dropped.

Saving refuses (400) a manual workflow with a config, an event config without event types, a subject.created config with event types and a filter that does not compile or does not answer a boolean. An event or subject.created workflow saved without a config, as workflows were before triggers started runs, keeps its trigger as a label: it starts nothing, and the builder's check (POST /workflow-definitions/validate) warns trigger_not_armed. In the console the workflow's properties hold the trigger and its config, and Start runs automatically arms it.

Drafts start nothing; the published version's config is the one that counts, read when an event is stored and again when its run starts. Publishing a version with the trigger manual stops it: starts it had queued are cancelled.

The filter ​

The filter reads the event and the subject as they are when the run starts:

  • event: id, type, amount, currency, occurredAt, attributes, ip, deviceId, cardFingerprint, email. Empty for subject.created.
  • subject: id, kind, externalId, status, tags, riskScore, data.
  • now, and the functions daysSince, lower, upper and toNumber.

A filter runs under the same step budget as a rule's expression. One that fails (a missing field read without has(), a string compared with a number) gives the start up as failed: guard optional fields, as in has(event.attributes.mcc) && event.attributes.mcc == "7995".

What a triggered run gets ​

The run's trigger is event or subject (and decide for a POST /decide escalation), and its context starts with what started it besides subjectId and subject:

json
{
  "trigger": {
    "source": "event",
    "event": { "id": "4133", "type": "payment", "amount": 6200, "currency": "EUR", "occurredAt": "…", "attributes": { … } }
  }
}

so its rules read trigger.event.amount as they read a step's output. A velocity rule in the run counts the triggering event too: it is stored before the run starts.

Exactly once ​

The start is written in the transaction that stores its source: an event that is stored has its starts queued, and a duplicate (the same idempotencyKey) queues nothing. Each queued start names its run's id before it starts, so a worker that dies mid-start or a retry starts the same run, once. The worker's trigger dispatcher takes queued starts up within a second; every worker runs it.

Following the starts ​

GET /workflow-triggers (runs:read) lists the queued starts newest first, filtered by definitionKey, status, subjectId and from/to; the console shows a workflow's latest under its trigger.

StatusMeaning
pendingWaiting for the dispatcher, or for its debounce window or the rate cap (dueAt).
startingIts run is being started; a failed start is tried again, up to 10 times, backing off.
startedIts run started (runId).
coalescedA newer match of its subject's debounce window started the run (runId) for it.
filteredThe filter or subjectKinds kept it out.
cancelledThe workflow is no longer published with that trigger or event type, or its source is gone.
failedThe filter failed, or the run did not start in 10 attempts (lastError).

Operators watch the dispatcher with aletheia.workflow.trigger.count and aletheia.workflow.trigger.lag, and three alerts (Operations).

Released under the Apache-2.0 License.