Trial onboarding journey (Journeys + Analytics + Outreach)
Cross-module workflow: the product's own backend reports trial sign-ups and invitations; a journey turns those signals into a welcome email, a sales flag for decision-makers and a help email for people who stall. It is the reference journey for a first launch, because it touches every part of the engine: an event trigger, a timed wait on working days, behaviour and field branches, an action, fixed-text emails that send without a review per person, a reply path and a goal.
This is a playbook — it composes per-module skills rather than reproducing them. Read skill://journeys/build-journey before executing.
TL;DR
- Make sure the product sends
trial.started,teammate.invitedandsubscription.startedtoPOST /v1/events. - Pick the email channel to send from and create a "Trialists" segment.
- Build the journey below with the
journeys_*tools, validate and simulate it. journeys_request_publishand hand thereviewUrllink to the operator. They confirm in the dashboard.- Watch
journeys_get_reportdaily for the first two weeks; the launch gate is met when integrity stays at zero.
Step 1 — the events
analytics_list_product_event_names shows which names have arrived in the last 30 days. You need:
| Event | Sent when | Used for |
|---|---|---|
trial.started | A trial account is created | Entry |
teammate.invited | The trial user invites a colleague | Branch: did they invite anyone since entering? |
subscription.started | The account starts paying | Goal |
If any is missing, tell the operator which event their developers must send. The request is POST /v1/events with an admin API key that has the analytics:write scope, a body { "events": [{ "id", "name", "occurredAt", "identity": { "email" | "externalId" | "contactId" }, "properties", "consent" }] }, and the caller's own unique id per event so retries never count twice.
trial.started should carry the lawful basis the sign-up gave, for example "consent": { "lawfulBasis": "contract", "source": "trial-signup" }. The key then also needs the crm:write scope. Munin records the basis on the person before the event starts the journey, so the welcome email can go out at once. For an email it has never seen it creates the contact first, so the identity must include email. A basis already on record is kept ("consent": "kept" in the result), and recording one never lifts an unsubscribe or a do-not-contact mark. Without consent an event creates no contact and records no basis, so nobody new gets these emails. occurredAt may not be more than five minutes in the future, and an organization can send at most 3000 events a minute (more answers 429 with retryAfterSeconds). Do not build the journey on events that never arrive.
Step 2 — audience and sender
- A segment of people who may receive onboarding email (
crm_create_segment).events: [{ "name": "trial.started", "occurred": true }]matches everyone the product has reported, including people created by the event itself; a tag filter such astagsAny: ["trial"]only works when something tags them. Segment members must have a recorded lawful basis; contacts without one never enter. - A lawful basis for every trial user. A trial sign-up is usually
contract(they asked for the trial). The sign-up sync should send it withtrial.started(Step 1). For people who signed up before it did, record it withcrm_set_contact_consent(lawfulBasis: "contract",source: "trial-signup"). Until it is recorded the welcome email skips them, andjourneys_simulatewarns withno_lawful_basisand how many people in its sample are affected. - An active email channel to send from (
conv_list_channels, typeemail). It becomes the journey'ssenderChannelId.
Step 3 — the journey
Create it with journeys_create, the email channel as senderChannelId and this spec, replacing the segment id, then adjust copy and timing with the operator:
{
"entry": { "trigger": { "type": "event", "event": "trial.started" }, "segmentId": "<trialists segment id>" },
"goal": { "event": "subscription.started", "windowDays": 30 },
"exit": { "idleDays": 30, "on": [] },
"settings": { "timezone": "Europe/Oslo", "quietHours": "20:00-08:00", "holdoutPct": 10, "frequencyCapPerWeek": 3 },
"start": "welcome",
"nodes": [
{ "id": "welcome", "type": "message", "channel": "email",
"content": { "subject": "Welcome to the trial", "body": "Hi {{name}}, here are three things to try today." },
"approval": "auto", "next": "wait5" },
{ "id": "wait5", "type": "wait", "duration": "P5D", "validDays": ["mon","tue","wed","thu","fri"], "validHours": "09:00-16:00", "next": "invited" },
{ "id": "invited", "type": "branch",
"rules": [{ "if": { "event": "teammate.invited", "since": "entry" }, "next": "done" }],
"default": "decision_maker" },
{ "id": "decision_maker", "type": "branch",
"rules": [{ "if": { "field": "contact.title", "op": "in", "value": ["CEO", "CTO", "Founder"] }, "next": "flag_sales" }],
"default": "offer_help" },
{ "id": "flag_sales", "type": "action", "action": "crm.add_tag", "params": { "tag": "trial-sales-lead" }, "next": "done" },
{ "id": "offer_help", "type": "message", "channel": "email",
"content": { "subject": "Can we help you get started?", "body": "Hi {{name}}, most teams get more out of a trial once a colleague joins. Want a hand setting it up?" },
"approval": "auto", "next": "done", "onReply": "replied" },
{ "id": "replied", "type": "action", "action": "crm.log_activity", "params": { "subject": "Replied to trial help email" }, "next": "done" },
{ "id": "done", "type": "exit" }
]
}
Both emails are fixed text, so they use auto: the operator reads them once in the publish review, and after that they go out to each trial user without a review per person. They still obey the send rules — consent, unsubscribe, quiet hours and the weekly cap. Switch a step to draft_only only when the operator wants to read it for every person, and never change a step's approval mode without their say.
Then journeys_validate (no errors) and journeys_simulate over the segment. Report the counts, the email decisions and the assumptions to the operator; ask about anything surprising before you go on.
Step 4 — publish
journeys_request_publish, then give the operator the reviewUrl link. A signed-in person confirms in the dashboard; you cannot, and editing the draft afterwards voids the request.
Step 5 — watch it until the launch gate is met
For the first two weeks, once a day:
journeys_get_report:integrity.duplicateSendsmust be 0 — no step ever sent two emails.integrity.enrollmentsWithoutTracemust be 0 — everyone who entered has a logged step.enrollments.exitReasonsshould be mostlyreached_endandgoal_reached. Investigatemessage_blocked,message_failed,proposal_dismissed,approval_timeoutorloop_detectedwithskill://journeys/investigate-enrollment.messagesshows how many emails were sent, are pending review or were dismissed per step. A growingpendingcount means reviews are falling behind.system_alerts_listfor ajourneysalert saying the journey is holding emails. That happens when an email template anautostep uses changes after publish: its emails wait, nothing goes out with the unreviewed template, and a signed-in person re-approves the journey with the new template from its dashboard page. Tell the operator; you cannot re-approve it.
outreach_list_proposalsfor journey emails waiting on review, if the operator wants a nudge.- Report goal conversion for the journey against the holdout once both groups have a few dozen people; before that the difference is noise.
The gate is met when both integrity numbers stayed at zero for two weeks of live traffic. Tell the operator, and propose any change as a new draft rather than editing what runs.