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 type | Fires when | Payload |
|---|---|---|
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:
{
"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.