Skip to content

cross-app-pitch

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

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.

Cross-sell between your own products, portfolio onboarding (“you use app A, try app B”), upgrade prompts timed to a billing event.

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.

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" },
});
},
});

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.

cross-app-pitch.json
{
"user": {
"id": "usr_1",
"traits": { "email": "[email protected]", "plan": "free" }
},
"events": [],
"expect": [
{
"activity": "sendEmail",
"input": {
"template": "cross-app-pitch",
"props": { "plan": "pro" }
}
}
]
}
Terminal window
# copy it into your project
cow add example cross-app-pitch
# run its scenario on a virtual clock
cow test cross-app-pitch --scenario scenarios/cross-app-pitch.json