Sending events

Engagement signals

Your events tell Flyhalf what a customer did. Signals are the events Flyhalf emits itself for what a customer didn't do — they stopped coming, or a streak is about to end. A flow triggers on a signal exactly as it would on order.completed.

The signals

Event typeFires whenPayload
participant.inactive A customer has done nothing for 7, 14, 30, 60 or 90 days. days_inactive, last_activity_at
participant.returned A customer who had gone quiet acts again. days_away, after_days_inactive, returned_with
streak.at_risk The last period that can keep a streak alive is closing. streak_key, streak_name, current_count, period, ends_at
streak.broken That period closed without a qualifying event. streak_key, streak_name, previous_count, best_count, period
quest.progressed A customer moves a step forward in a challenge, but has not finished it. quest_id, quest_name, done, required, remaining
quest.completed A customer finishes a challenge. quest_id, quest_name, done, required, remaining

The quiet-customer and streak signals are checked hourly. The two challenge events fire straight after the event that moved the challenge. You never send any of them, and they don't count against your event quota.

Triggering on one milestone

participant.inactive fires at every milestone, so filter the trigger to the one you want. This flow runs once each time a customer reaches 30 quiet days:

Flow trigger json
{
  "type": "event",
  "event_type": "participant.inactive",
  "filters": [
    { "field": "payload.days_inactive", "op": "=", "value": 30 }
  ]
}

What counts as activity

Any event you send moves a customer's last_activity_at to its occurred_at, and so does a click on a tracked link in a Flyhalf message. Flyhalf's own reactions — an achievement unlocking, a signal firing — never do, so a win-back message can't make its own recipient look active. order.refunded is ignored too: that is you acting, not the customer.

last_activity_at is returned on GET /v1/participants/{external_id}. Backfilled events with an old occurred_at never rewind it.

Imports don't trigger a burst

A milestone only fires while it is fresh — within three days of being crossed. Import a customer whose last order was 200 days ago and they're recorded as quiet, but no signal fires and no win-back flow runs for them.

Streak timing

A daily streak is at risk in the last six hours of the day it must be kept by; a weekly streak in the last two days of its week. Streaks shorter than two periods end without a signal. Days and weeks start and end in your workspace's time zone, set under Workspace → Settings. It is UTC until you change it.

Progress messages

Trigger a flow on quest.progressed to tell a customer how far they are: {{event.payload.done}} of {{event.payload.required}}, {{event.payload.remaining}} to go. Filter the trigger on payload.quest_id to limit it to one challenge.

Sandbox

Sandbox customers receive signals too, flagged as sandbox, so a signal-triggered flow can be exercised end to end with an fh_test_ key. Messages from those runs are simulated, never sent. To try a flow without waiting a week, use the Flow Builder's test run — every signal ships with a sample payload.