# Triggers and exits

How a flow starts and stops, field by field: the trigger event and the one source it should listen to, Instasent's own events, the event filter, who may enter, the re-entry limits, the exits that take a contact out, and how goals are counted.

**Language:** en
**Audience:** developer
**TLDR:** Every flow has one trigger, TRIGGER_CONTACT_EVENT: an event type plus eventDatasource. Set eventDatasource to the source marked preferred for that event in POST /event/volumes; left null, the trigger fires once per copy when several sources send the same event. Only Instasent's own events (platformEventTypes, such as update or subscribe) stay null. Exits live in the trigger's exitSteps and end a contact's run; useAsGoal makes an exit count as a goal.
**Search keywords:** trigger, flow trigger, trigger event, TRIGGER_CONTACT_EVENT, eventType, eventDatasource, preferred source, preferred datasource, duplicate trigger, fires twice, relayed events, platform events, platformEventTypes, update event, new contact, welcome trigger, joined list, tag added, subscribe, unsubscribe, eventQueryFilter, event filter, fill rate, fillRate, audienceQueryFilter, excludeQueryFilters, re-entry, reentry, frequency cap, maxActivationsPerContact, minSecBetweenActivationsPerContact, maxConcurrentActiveFlowsPerContact, exit, exit event, exitSteps, stop flow, goal, useAsGoal, conversion goal, schedule trigger
**Related pages:** /developers/product-api/flows/overview, /developers/product-api/audience/probes, /developers/product-api/audience/events
**Docs index (every page):** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/llms.txt
**This zone's index:** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/developers/product-api/llms-full.txt
**This page:** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/developers/product-api/flows/triggers-and-exits/ (HTML) · https://staging-instasent-docs-nextjs.oscar-284.workers.dev/developers/product-api/flows/triggers-and-exits.md (Markdown)

Every flow has exactly one trigger, the root of the graph, and through the API it is always `TRIGGER_CONTACT_EVENT`: the flow starts when a contact produces an event. Whether a flow works depends less on the shape of the graph than on this node: whether its event really arrives, from which source, and what stops the journey once its purpose is met.

A trigger that fires on a schedule exists in the product but cannot be placed through the API; a draft carrying one is refused.

## The event and its source

- `eventType` — `string`, required
  The event type uid, such as `ecommerce_checkout_init`. Take it from the [event catalogue](/developers/product-api/audience/events) or from the [event volumes probe](/developers/product-api/audience/probes#event-volumes), never from a display name. Only types whose `automation` is `true` in the catalogue can start a flow.
- `eventDatasource` — `string`
  The id of the **one** data source the trigger listens to. `null` listens to every source.

**Point the trigger at the event's preferred source.** When two sources send the same event — a shop and a marketing tool that relays the shop's orders, for example — they are copies of one fact, and nothing merges them. A trigger with no source fires once per copy, so the same contact enters the flow twice for one purchase.

The [event volumes probe](/developers/product-api/audience/probes#where-each-event-comes-from) lists, for each event type, the sources that sent it, most authoritative first, and marks at most one of them `preferred`. For commerce events the shop that owns the orders outranks a tool that relays them; for anything else the fresher and busier source wins. Set `eventDatasource` to that id. The [templates](/developers/product-api/flows/templates) do this for you.

> **Tip**: If the event has been declared but has not arrived yet, [`GET /datasource/{id}`](/developers/product-api/reference) tells you what a connected integration is able to send: `metadata.capabilities.events` lists the event types it emits, and `delivery` says whether it pushes them as they happen (`realtime`) or they are fetched on a schedule (`periodic`), in which case they arrive in batches.

Some events are late by design. A shop's `ecommerce_checkout_abandon` is typically raised hours after the checkout stops changing, so a reminder triggered on it already carries that delay. Triggering on `ecommerce_checkout_init` and adding your own [delay](/developers/product-api/flows/delays), with an exit on the order, is the faster and more common choice.

## Instasent's own events

Some events are raised by the platform itself rather than by a source: the contact changed, subscribed, unsubscribed, clicked a tracked link. The probe lists them apart, in `platformEventTypes`, and they are the one case where `eventDatasource` stays `null`: they are raised on whatever source the contact belongs to, so no source is preferred.

| Event                                                             | Starts a flow when                                                                                                                                                                                                                                                                                                                 |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `update`                                                          | The contact changed. Filtered on `update.new-contact` being `true`, it is a contact **created** in that source: the welcome trigger, not `subscribe`. Filtered on `update.lists-added` or `update.tags-added` (the exact list or tag name), it is "joined list X" or "gained tag Y"; `lists-lost` and `tags-lost` are the reverse. |
| `subscribe`, `unsubscribe`                                        | The contact's consent changed.                                                                                                                                                                                                                                                                                                     |
| `campaign_cta`, `automation_cta`, `flow_cta`, `transactional_cta` | A tracked link in a message was clicked.                                                                                                                                                                                                                                                                                           |

Sends, opens, contact creation, merges and deletions are recorded as events too, but they cannot start a flow.

## The event filter

`eventQueryFilter` narrows the trigger to some occurrences of the event, by the event's own parameters. An empty value (`[]`) means every occurrence. Otherwise it is one `root` group in the [event query filter](/developers/product-api/audience/event-query-filter) grammar, and every key is `<eventType>.<parameter>`:

```json
{
  "root": {
    "type": "group",
    "join": "and",
    "children": [
      {
        "type": "event_condition",
        "key": "ecommerce_order_create.order-euro-amount",
        "operator": "range-number",
        "values": { "lowerNumber": 100 }
      }
    ]
  }
}
```

Because every key is prefixed with the event type, a filter belongs to one event: change `eventType` and rewrite the filter.

**Filter only on values that actually arrive.** Probe the event first with `"event": "<eventType>"` on [`/event/volumes`](/developers/product-api/audience/probes#one-level-deeper-what-the-parameters-carry): it lists the real values of each parameter and its `fillRate`, the share of events that carry it at all. A filter on a value that never occurs, or on a parameter the source never fills (product tags are empty on many stores), produces a flow that is valid and never fires.

## Who may enter

- `audienceQueryFilter` — `object`
  An [audience query filter](/developers/product-api/audience/query-filter) the contact must match when entering. Empty means everyone. To use a segment, carry its uid in `metadata.segment.uid` next to its `root`: a filter tied to a segment is refreshed from that segment when the flow is published and while it runs, while one without a uid is kept exactly as written. The templates use `dyn-generic-has-phone` this way, so nobody without a mobile phone enters a journey that can never send.
- `excludeQueryFilters` — `object[]`
  Filters in the same shape. A contact matching any of them does not enter.
- `cancelIfExitsAudienceSegment` — `boolean`
  When `true`, a contact who stops matching the audience while inside the flow, exclusions included, is taken out.

Segment uids come from [`GET /segment`](/developers/product-api/audience/segments).

## Re-entry and caps

A contact who hits any of these limits does not enter, and the attempt is dropped, not queued.

- `maxActivationsPerContact` — `integer`
  Entries per contact over the flow's lifetime, across every version. `0` means unlimited; `1` means once ever, which is what a welcome wants.
- `minSecBetweenActivationsPerContact` — `integer`
  Seconds that must pass since this contact last entered. A checkout reminder uses a day, so it does not run twice for the same shopper in one afternoon.
- `maxConcurrentActiveFlowsPerContact` — `integer`
  Runs of **this** flow one contact may have in progress at once. It is not a limit across flows: two flows on the same trigger both run.
- `maxActivationsPerHour` — `integer`
  Entries into the whole flow per hour, all contacts together.
- `maxActivationsPerDay` — `integer`
  Entries into the whole flow per day, all contacts together.

The plan also applies its own ceiling on entries when the flow runs, so the effective cap can be lower than the value stored.

Entry is not instantaneous: each entry waits a short moment so the flow reads the contact as it is after the trigger event, and under heavy traffic entries may be held a little longer — never beyond the waits the flow already has before its first send, so send times do not move.

## Exits

Exits live on the trigger, in `exitSteps`, never in the chain of steps. Each one is an `ACTION_SEQUENTIAL_EXIT_TRIGGER_EVENT` with the same fields as the trigger — `eventType`, `eventDatasource`, `eventQueryFilter` — and the same rules: prefix filter keys with the exit's own event type, and point it at the same preferred source as the trigger, or it will also fire on relayed copies. A trigger holds at most 10 exits.

```json
{
  "stepClass": "TRIGGER_CONTACT_EVENT",
  "eventType": "ecommerce_checkout_init",
  "eventDatasource": "66c3a1b2e5a6b7c8d9e0f1a2",
  "exitSteps": [
    {
      "stepClass": "ACTION_SEQUENTIAL_EXIT_TRIGGER_EVENT",
      "eventType": "ecommerce_order_create",
      "eventDatasource": "66c3a1b2e5a6b7c8d9e0f1a2",
      "eventQueryFilter": [],
      "useAsGoal": true
    }
  ],
  "maxConcurrentActiveFlowsPerContact": 1,
  "minSecBetweenActivationsPerContact": 86400,
  "nextStep": { "...": "..." }
}
```

- **An exit event received while the contact is inside the flow ends the run.** Nothing further is sent.
- **An exit event that already arrived when the contact is about to enter keeps them out.** Since entry happens shortly after the trigger rather than at the same instant, a shopper who orders moments after abandoning a checkout is never messaged.
- **An exit on the trigger's own event restarts the clock.** The event that started the run never ends it; the next one does, and starts a fresh run if the re-entry limits allow. That is how a winback waits from the contact's **last** order rather than their first.

Every reminder needs a way out when its purpose happens: a checkout or product journey without an exit on `ecommerce_order_create` is flagged by the [draft advice](/developers/product-api/flows/drafts#advice).

## Goals

A goal is how a flow measures that it did its job.

- **`useAsGoal: true` on an exit** makes it a goal: the run ends by goal instead of a plain exit. Set it only on exits that are objectives — purchased, booked — and never on housekeeping exits such as `unsubscribe`, or the goal figures inflate.
- **`ACTION_SEQUENTIAL_GOAL`**, a node in the graph, marks a goal by position: the contact reached this point. It does not send anything or stop the run. See [Nodes](/developers/product-api/flows/nodes#sequential-nodes).

A goal counts only if the run had already sent the contact at least one message; one reached before any message is reported apart. The goal rate is runs with a goal over runs that sent at least one message, so it never exceeds 100%. A goal happens at most once per run and carries no money: revenue and conversions come from [attribution](/developers/product-api/analytics/conversions), so a flow's goal count and its conversion count can legitimately differ.

## What's next

- [Nodes](/developers/product-api/flows/nodes) - What goes after the trigger: sends, branches and the other nodes.
- [Audience probes](/developers/product-api/audience/probes) - The event volumes probe: sources per event, the preferred one, and fill rates.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://staging-instasent-docs-nextjs.oscar-284.workers.dev/llms.txt — start there for full context.
