cross-app-pitch
What it does
Section titled “What it does”Cross-app pitch: plan_upgraded from one app (the trigger’s appId field
scopes a journey to one app of a multi-app org) starts the execution; after
a durable day it re-reads the profile and pitches the other app only to
users still on the free plan. This is the portfolio-operator pattern: one
org’s journeys reacting across its own apps.
Example: add it with cow add example cross-app-pitch; rename the app id,
the event, and the email to your own.
| Trigger | { event: "plan_upgraded", appId: "app_b" } |
| Tags | demo, cross-app |
| Consent purpose | emailMarketing |
The pattern
Section titled “The pattern”App-scoped trigger plus a fresh profile read. plan_upgraded from one app (the trigger’s appId field) starts the execution; after a durable day, api.profile.get() re-reads the profile and pitches the other app only to users still on the free plan. This is the portfolio-operator pattern: one org’s journeys reacting across apps.
Reach for it when
Section titled “Reach for it when”Cross-sell between your own products, portfolio onboarding (“you use app A, try app B”), upgrade prompts timed to a billing event.
Make it yours
Section titled “Make it yours”Rename the app id, the event, and the email. api.profile.get() is a fresh read on every call, so the branch sees the plan as it is after the wait, not as it was at the trigger. Omit appId to react across every app.
The journey
Section titled “The journey”import { defineJourney } from "@cowliss/cli/journeys";
/** * Cross-app pitch: `plan_upgraded` from one app (the trigger's `appId` field * scopes a journey to one app of a multi-app org) starts the execution; after * a durable day it re-reads the profile and pitches the other app only to * users still on the free plan. This is the portfolio-operator pattern: one * org's journeys reacting across its own apps. * * Example: add it with `cow add example cross-app-pitch`; rename the app id, * the event, and the email to your own. */export default defineJourney({ trigger: { event: "plan_upgraded", appId: "app_b" }, purpose: "emailMarketing", tags: ["demo", "cross-app"], run: async (_event, api) => { await api.sleep("1d"); // A fresh read, not a snapshot taken at the trigger: the plan may well // have changed during the wait, which is the whole point of waiting. const me = await api.profile.get(); if (me.traits.plan !== "free") { return; } await api.email.send({ template: "cross-app-pitch", props: { plan: "pro" }, }); },});Its scenario
Section titled “Its scenario”The example ships this scenario, copied in beside the journey. cow test replays it on a virtual clock: the trigger fires at the start, events arrive at their offsets, and expect is the ordered list of capability calls the run must make, each input matched as a subset.
{ "user": { "id": "usr_1", }, "events": [], "expect": [ { "activity": "sendEmail", "input": { "template": "cross-app-pitch", "props": { "plan": "pro" } } } ]}Try it
Section titled “Try it”# copy it into your projectcow add example cross-app-pitch
# run its scenario on a virtual clockcow test cross-app-pitch --scenario scenarios/cross-app-pitch.json