@apex-inc/mcp-server 0.3.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +8 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +23 -0
- package/dist/api-client.js.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/tools.d.ts +644 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +713 -5
- package/dist/tools.js.map +1 -1
- package/package.json +9 -3
- package/skills/apex-communications/SKILL.md +40 -0
- package/skills/apex-experimentation/SKILL.md +9 -0
- package/skills/apex-growth-tracking/SKILL.md +59 -3
- package/skills/apex-integration-cookbook/SKILL.md +167 -141
- package/skills/apex-journeys/SKILL.md +169 -0
- package/skills/apex-partner-network/SKILL.md +191 -0
|
@@ -1,53 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apex-integration-cookbook
|
|
3
|
-
description: Recipes for integrating Apex into products —
|
|
3
|
+
description: Recipes for integrating Apex into products — SDK wiring, form tracking, auth/signup events, Stripe lifecycle, communication triggers, and event naming conventions. Use when the user is adding Apex tracking to their product or wiring events to communications and experiments.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Apex integration cookbook
|
|
6
|
+
# Apex integration cookbook (SDK)
|
|
7
7
|
|
|
8
|
-
Step-by-step recipes for
|
|
8
|
+
Step-by-step recipes for integrating Apex via the `@apex-inc/sdk` package. For snippet-only sites or MCP-driven setup, see the full cookbook in `@apex-inc/mcp-server`.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## SDK setup
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
**Goal:** Track visits, form submissions, and run copy experiments on a marketing site.
|
|
22
|
-
|
|
23
|
-
```html
|
|
24
|
-
<script src="https://your-apex-url/api/apex-js?key=YOUR_PROJECT_KEY" defer></script>
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
What you get automatically:
|
|
28
|
-
- `pageview` events with URL, referrer, UTM params
|
|
29
|
-
- `heartbeat` events (engagement time)
|
|
30
|
-
- Visitor identity via `apex_vid` cookie
|
|
31
|
-
- Attribution from UTM parameters and click IDs (gclid, fbclid, etc.)
|
|
32
|
-
|
|
33
|
-
Add form tracking:
|
|
34
|
-
```javascript
|
|
35
|
-
apex.track("form_submit", {
|
|
36
|
-
formId: "signup-form",
|
|
37
|
-
email: formData.email
|
|
12
|
+
```typescript
|
|
13
|
+
import { init, track, identify, trackForm, shutdown } from "@apex-inc/sdk";
|
|
14
|
+
|
|
15
|
+
init({
|
|
16
|
+
projectKey: "YOUR_PROJECT_KEY",
|
|
17
|
+
apiUrl: "https://your-apex-url", // optional, defaults to relative /api
|
|
18
|
+
flushInterval: 10000, // ms between batch sends (default: 10s)
|
|
19
|
+
flushAt: 20, // batch size trigger (default: 20)
|
|
20
|
+
debug: false // log events to console
|
|
38
21
|
});
|
|
39
|
-
apex.identify(formData.email, { source: "form", formId: "signup-form" });
|
|
40
22
|
```
|
|
41
23
|
|
|
42
|
-
|
|
24
|
+
Events are batched and sent to `POST /api/events` automatically. Call `flush()` before page unload if needed, or `shutdown()` to stop tracking.
|
|
43
25
|
|
|
44
|
-
|
|
26
|
+
## Recipe 1: Signup and authentication
|
|
45
27
|
|
|
46
28
|
```typescript
|
|
47
|
-
import { init, track, identify } from "@apex-inc/sdk";
|
|
48
|
-
|
|
49
|
-
init({ projectKey: "YOUR_PROJECT_KEY" });
|
|
50
|
-
|
|
51
29
|
// On signup
|
|
52
30
|
track("signup_completed", { method: "email", plan: "free" });
|
|
53
31
|
identify(user.id, {
|
|
@@ -61,154 +39,202 @@ track("login", { method: "google" });
|
|
|
61
39
|
identify(user.id, { email: user.email });
|
|
62
40
|
```
|
|
63
41
|
|
|
64
|
-
The `visitorId` trait
|
|
42
|
+
The `visitorId` trait links the anonymous marketing session to the authenticated user. Without it, pre-signup attribution is lost.
|
|
65
43
|
|
|
66
|
-
## Recipe
|
|
67
|
-
|
|
68
|
-
**Goal:** Track onboarding steps to measure activation and trigger communications.
|
|
44
|
+
## Recipe 2: Onboarding funnel
|
|
69
45
|
|
|
70
46
|
```typescript
|
|
71
|
-
// Each onboarding step
|
|
72
47
|
track("onboarding_step_completed", { step: "connect_data_source", stepNumber: 2, totalSteps: 5 });
|
|
73
|
-
|
|
74
|
-
// Onboarding complete — this triggers activation communications
|
|
75
48
|
track("onboarding_completed", { timeToComplete: elapsedMs });
|
|
76
49
|
```
|
|
77
50
|
|
|
78
|
-
Events like `onboarding_completed` automatically match to
|
|
51
|
+
Events like `onboarding_completed` automatically match to active tenant communications when the trigger system is wired.
|
|
79
52
|
|
|
80
|
-
## Recipe
|
|
53
|
+
## Recipe 3: Stripe subscription lifecycle
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// In your Stripe webhook handler
|
|
57
|
+
track("subscription_created", { plan: "pro", amount: 9900, currency: "usd" });
|
|
58
|
+
identify(userId, { plan: "pro", lifecycleStage: "paying" });
|
|
81
59
|
|
|
82
|
-
|
|
60
|
+
// On cancellation
|
|
61
|
+
track("subscription_cancelled", { reason: "too_expensive" });
|
|
62
|
+
identify(userId, { lifecycleStage: "churned" });
|
|
63
|
+
```
|
|
83
64
|
|
|
84
|
-
|
|
65
|
+
## Recipe 4: Feature usage
|
|
85
66
|
|
|
86
67
|
```typescript
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
case "checkout.session.completed":
|
|
90
|
-
track("subscription_created", {
|
|
91
|
-
plan: session.metadata.plan,
|
|
92
|
-
amount: session.amount_total,
|
|
93
|
-
currency: session.currency
|
|
94
|
-
});
|
|
95
|
-
identify(session.client_reference_id, {
|
|
96
|
-
email: session.customer_email,
|
|
97
|
-
plan: session.metadata.plan,
|
|
98
|
-
lifecycleStage: "paying"
|
|
99
|
-
});
|
|
100
|
-
break;
|
|
101
|
-
|
|
102
|
-
case "invoice.paid":
|
|
103
|
-
track("invoice_paid", {
|
|
104
|
-
amount: invoice.amount_paid,
|
|
105
|
-
plan: invoice.lines.data[0]?.price?.lookup_key
|
|
106
|
-
});
|
|
107
|
-
break;
|
|
108
|
-
|
|
109
|
-
case "customer.subscription.deleted":
|
|
110
|
-
track("subscription_cancelled", { reason: subscription.cancellation_details?.reason });
|
|
111
|
-
identify(customerId, { lifecycleStage: "churned" });
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
68
|
+
track("feature_used", { feature: "export_report", format: "csv" });
|
|
69
|
+
track("milestone_reached", { milestone: "first_experiment_completed" });
|
|
114
70
|
```
|
|
115
71
|
|
|
116
|
-
|
|
72
|
+
Use one event name with a `feature` property — not dozens of unique event names.
|
|
117
73
|
|
|
118
|
-
|
|
74
|
+
## Recipe 5: Form tracking (shortcut)
|
|
119
75
|
|
|
120
76
|
```typescript
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
77
|
+
trackForm({
|
|
78
|
+
email: formData.email,
|
|
79
|
+
formId: "contact-form",
|
|
80
|
+
action: "demo_request",
|
|
81
|
+
fields: { company: formData.company, role: formData.role }
|
|
82
|
+
});
|
|
83
|
+
```
|
|
125
84
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
85
|
+
`trackForm` fires `form_submit` + `identify` in one call.
|
|
86
|
+
|
|
87
|
+
## Recipe 6: Server-side events (Server Events API)
|
|
88
|
+
|
|
89
|
+
For backend events (webhooks, cron jobs, CRM milestones), use the **Server Events API**: `sendServerEvent` / `sendServerEvents` from `@apex-inc/sdk`. This is the canonical replacement for raw `fetch()` calls to `/api/events`.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { sendServerEvent, newIdempotencyKey } from "@apex-inc/sdk";
|
|
93
|
+
|
|
94
|
+
// Stripe webhook example
|
|
95
|
+
await sendServerEvent(
|
|
96
|
+
{ apiKey: process.env.APEX_API_KEY! }, // apex_sk_…
|
|
97
|
+
{
|
|
98
|
+
type: "purchase_completed",
|
|
99
|
+
email: customer.email,
|
|
100
|
+
visitorId: cookieJar.get("apex_vid"), // optional but improves stitching
|
|
101
|
+
data: { value: invoice.amount_paid / 100, currency: "USD", order_id: invoice.id },
|
|
102
|
+
},
|
|
103
|
+
{ idempotencyKey: stripeEvent.id }, // dedupe on retry
|
|
104
|
+
);
|
|
129
105
|
```
|
|
130
106
|
|
|
131
|
-
|
|
107
|
+
Why prefer the Server Events API over `fetch("/api/events")`:
|
|
108
|
+
|
|
109
|
+
- **Idempotency-key** — retries don't double-count.
|
|
110
|
+
- **clientType: "server"** — the Conversion Loop dashboard classifies events correctly.
|
|
111
|
+
- **Single endpoint** — `/api/v1/events` is the documented, OpenAPI'd integration surface.
|
|
112
|
+
- **Typed errors** — the wrapper throws on non-2xx with a clear `error.code` from the API.
|
|
113
|
+
|
|
114
|
+
For batches > 1 event, use `sendServerEvents(config, events, { idempotencyKey })` (max 100 per call).
|
|
115
|
+
|
|
116
|
+
For agent-driven testing through MCP, use the **`send_server_event`** tool — same endpoint, exposed as a single-call MCP tool with the idempotency key generated for you.
|
|
117
|
+
|
|
118
|
+
## Event naming conventions
|
|
119
|
+
|
|
120
|
+
- **snake_case**: `signup_completed`, `feature_used`, `invoice_paid`
|
|
121
|
+
- Context in **properties**: `track("button_clicked", { surface: "pricing", label: "start_trial" })`
|
|
122
|
+
- For experiments, include `experimentId` and `variant` in properties
|
|
123
|
+
- One primary metric per experiment; secondary metrics as properties
|
|
124
|
+
|
|
125
|
+
## Identity stitching checklist
|
|
132
126
|
|
|
133
|
-
|
|
127
|
+
1. Call `identify(userId, { email, visitorId })` when the user becomes known
|
|
128
|
+
2. `visitorId` = value of the browser `apex_vid` cookie
|
|
129
|
+
3. Server-side: pass `traits.visitorId` to link to the browser session
|
|
130
|
+
4. Without stitching, pre-signup attribution is lost
|
|
134
131
|
|
|
135
|
-
|
|
132
|
+
## Partner Network patterns
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
1. A `TenantCommunication` exists with `status: "active"` and a `triggerEventId`
|
|
139
|
-
2. An event matching that `triggerEventId` arrives via `apex.track()`
|
|
140
|
-
3. Multi-channel dispatch sends via the user's preferred channels (email, push, in-app)
|
|
134
|
+
### Credit a partner for a self-serve signup
|
|
141
135
|
|
|
142
|
-
|
|
136
|
+
When a visitor arrives from an affiliate link (`?utm_source=affiliate&utm_campaign=<handle>`) and signs up on your site, the Apex snippet automatically stitches the referral to the resulting Contact. The conversion auto-credits on the next purchase event (MMP-096 wiring).
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
|-------|--------------|
|
|
146
|
-
| `signup_completed` | Welcome email |
|
|
147
|
-
| `onboarding_completed` | Activation congratulations |
|
|
148
|
-
| `feature_used` (first time) | Feature discovery nudge |
|
|
149
|
-
| `subscription_created` | Payment confirmation |
|
|
150
|
-
| `trial_expiring` | Upgrade prompt |
|
|
151
|
-
| `user_inactive_7d` | Re-engagement email |
|
|
138
|
+
No SDK calls needed for basic attribution. Use `list_partner_program_members(programId)` to see who's actually earning.
|
|
152
139
|
|
|
153
|
-
|
|
140
|
+
### Run a manual-approval program + vet applicants in a script
|
|
154
141
|
|
|
155
|
-
|
|
142
|
+
```typescript
|
|
143
|
+
import { createManagementClient } from "@apex-inc/sdk/management";
|
|
144
|
+
const apex = createManagementClient({ projectKey, apiKey });
|
|
145
|
+
|
|
146
|
+
// Create the program
|
|
147
|
+
const program = await apex.createProgram({
|
|
148
|
+
name: "Vetted Partners",
|
|
149
|
+
vertical: "saas",
|
|
150
|
+
commissionStructure: { type: "revshare", percentage: 25, appliesTo: "subscription" },
|
|
151
|
+
visibility: "public",
|
|
152
|
+
approvalMode: "manual", // every applicant needs review
|
|
153
|
+
});
|
|
156
154
|
|
|
157
|
-
|
|
155
|
+
// ...applicants land in /admin/partners/applications for human review.
|
|
156
|
+
// The SDK doesn't wrap the approval flow — that's an admin-auth path.
|
|
157
|
+
// But merchants can monitor who's joined:
|
|
158
|
+
const members = await apex.listProgramMembers(program.id);
|
|
159
|
+
```
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
### Batch-approve the weekly payout queue
|
|
160
162
|
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
163
|
+
```typescript
|
|
164
|
+
const queue = await apex.listPendingPayouts();
|
|
165
|
+
const olderThanThirtyDays = queue.buckets["30_60"].concat(queue.buckets["60_plus"]);
|
|
166
|
+
const result = await apex.approvePayouts(olderThanThirtyDays.map(c => c.id));
|
|
167
|
+
console.log(`Approved ${result.approved.length}, skipped ${result.skipped.length}`);
|
|
167
168
|
```
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
**Approving does not fire Stripe transfers.** The next `run-payouts` cron cycle (per your `autoPayoutPolicy`) batches + fires. Design reason: reserve holdback window (90 days for NEW partners) has to elapse before transfer.
|
|
171
|
+
|
|
172
|
+
### Give an enterprise partner a custom rate
|
|
170
173
|
|
|
171
|
-
|
|
174
|
+
```typescript
|
|
175
|
+
await apex.updateMembershipRate("prof_acme", {
|
|
176
|
+
commissionStructure: { type: "revshare", percentage: 35, appliesTo: "subscription" },
|
|
177
|
+
reason: "Enterprise negotiation - Q4 contract",
|
|
178
|
+
});
|
|
179
|
+
```
|
|
172
180
|
|
|
173
|
-
|
|
181
|
+
Overrides are sticky. Updating the program default later won't overwrite this partner's rate (only `applyProgramDefaultToMembers` can mass-update, and it respects sticky overrides).
|
|
174
182
|
|
|
175
|
-
|
|
183
|
+
### Disclose fees accurately in your UI
|
|
176
184
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
-H "x-apex-project: YOUR_PROJECT_KEY" \
|
|
182
|
-
-d '{
|
|
183
|
-
"projectKey": "YOUR_PROJECT_KEY",
|
|
184
|
-
"userId": "user_123",
|
|
185
|
-
"events": [{
|
|
186
|
-
"type": "track",
|
|
187
|
-
"payload": { "event": "invoice_paid", "amount": 9900, "currency": "usd" },
|
|
188
|
-
"timestamp": "2026-04-15T12:00:00Z"
|
|
189
|
-
}]
|
|
190
|
-
}'
|
|
185
|
+
```typescript
|
|
186
|
+
const { fees } = await apex.getEffectiveFees();
|
|
187
|
+
// Show merchants what they're charged per fee kind.
|
|
188
|
+
// source: "merchant_override" means they have a custom (enterprise) rate.
|
|
191
189
|
```
|
|
192
190
|
|
|
193
|
-
##
|
|
191
|
+
## Recipe — Mobile build sources (TestFlight vs App Store)
|
|
194
192
|
|
|
195
|
-
-
|
|
196
|
-
- Context in **properties**, not event names: `track("button_clicked", { surface: "pricing", label: "start_trial" })` — not `pricing_start_trial_clicked`
|
|
197
|
-
- For experiments, include `experimentId` and `variant` in properties when the event is relevant to exposure
|
|
198
|
-
- One primary metric per experiment; secondary metrics as properties
|
|
193
|
+
Apex auto-detects which build a mobile event came from and routes accordingly. Use the same `projectKey` everywhere — there's no separate sandbox key to provision.
|
|
199
194
|
|
|
200
|
-
|
|
195
|
+
### Detection (no SDK code required)
|
|
196
|
+
|
|
197
|
+
iOS uses StoreKit 2's `appStoreReceiptURL` to distinguish `app-store`, `testflight`, and `xcode-debug`. Android uses `PackageManager.getInstallSourceInfo()` (API 30+) to distinguish `play-production`, `play-internal`, and `sideloaded`, with `BuildConfig.DEBUG` overriding to `xcode-debug` for local builds.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { Apex } from "@apex-inc/capacitor-plugin";
|
|
201
|
+
|
|
202
|
+
await Apex.initialize({ projectKey: "wsk_..." });
|
|
203
|
+
// That's it. The next event includes `releaseChannel` + `releaseChannelSource`.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Override for Android closed-test tracks
|
|
207
|
+
|
|
208
|
+
iOS overrides are ignored (the receipt always wins). On Android, `getInstallerPackageName()` can't distinguish Play production from closed-test tracks, so you can supply a hint at init time:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { Apex } from "@apex-inc/capacitor-plugin";
|
|
212
|
+
import { ApexChannel } from "./build-config"; // BuildConfig.APEX_CHANNEL
|
|
213
|
+
|
|
214
|
+
await Apex.initialize({
|
|
215
|
+
projectKey: "wsk_...",
|
|
216
|
+
releaseChannel: ApexChannel, // "play-internal" for closed-test variant
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Use Gradle `buildConfigField` to inject the channel per build variant so the value is wired at compile time, not runtime.
|
|
221
|
+
|
|
222
|
+
### Billing implications
|
|
223
|
+
|
|
224
|
+
- **Dev** (Xcode-debug, Gradle-debug, sideloaded): always **free**.
|
|
225
|
+
- **Beta** (TestFlight, Play internal/closed): always **free**.
|
|
226
|
+
- **Production** (App Store, Play production): **billable**.
|
|
227
|
+
- **Unknown** (older SDK without detection): billable, but the dashboard surfaces a fix-it nudge with an SDK upgrade prompt.
|
|
228
|
+
|
|
229
|
+
No retroactive credits — if you misclassify a build, the fix takes effect for future events.
|
|
201
230
|
|
|
202
|
-
When
|
|
231
|
+
### When NOT to recommend a separate project
|
|
203
232
|
|
|
204
|
-
|
|
205
|
-
2. The `visitorId` trait links anonymous browsing to the authenticated user
|
|
206
|
-
3. Server-side `identify`: pass `traits.visitorId` = browser's `apex_vid` cookie value
|
|
207
|
-
4. Without stitching, Apex creates a disconnected identity — pre-signup attribution is lost
|
|
233
|
+
Some MMP vendors require two project keys (sandbox + live). Apex does not. If a merchant asks "should I use a different project for beta?" or "do I need a sandbox key?", the answer is **no**: point to `/docs/mobile/test-vs-production` and walk them through the **Build environments** card in Settings → Mobile apps.
|
|
208
234
|
|
|
209
235
|
## Related
|
|
210
236
|
|
|
211
|
-
- Event
|
|
237
|
+
- Event tracking fundamentals: see **apex-growth-tracking** skill
|
|
212
238
|
- Experiment design: see **apex-experimentation** skill
|
|
213
|
-
- Intelligence and beliefs: see **apex-growth-intelligence** skill
|
|
214
239
|
- Communication setup: see **apex-communications** skill
|
|
240
|
+
- Affiliate / partner programs: see **apex-partner-network** skill
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apex-journeys
|
|
3
|
+
description: Help users design, audit, and ship Adaptive Journeys — multi-step lifecycle sequences with waits, branches, and exit rules. Use whenever the user mentions journeys, cart abandonment, onboarding sequences, drip campaigns, or asks to add/remove an exit rule. Always run alongside apex-communications for any cart-recovery, lifecycle, or sequence work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Apex Journeys (MCP)
|
|
7
|
+
|
|
8
|
+
Journeys are multi-step communication sequences with waits, branches, and exit rules. A single `send_email` is a Communication; a `send_push → wait 1h → send_email → wait 24h → send_email` is a Journey.
|
|
9
|
+
|
|
10
|
+
The single hardest thing about journeys is **knowing when to stop**. The vast majority of lifecycle programs that look broken to end users are journeys that fired a "you forgot something" message after the user already came back. This skill makes exit rules a first-class concern.
|
|
11
|
+
|
|
12
|
+
## When to Activate
|
|
13
|
+
|
|
14
|
+
- User says "journey", "sequence", "drip", "lifecycle program", "cart abandonment", "abandoned cart recovery"
|
|
15
|
+
- User wants to chain multiple messages with delays between them
|
|
16
|
+
- User is editing or auditing exit rules
|
|
17
|
+
- User mentions branching on subject state (cart contents, plan tier, lifecycle stage)
|
|
18
|
+
- User just created a marketing communication and you sense it should be part of a sequence
|
|
19
|
+
|
|
20
|
+
## Tool Map
|
|
21
|
+
|
|
22
|
+
| Tool | When to use |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `list_journeys` | Browse all journeys with their exit-rule counts. Start here for any audit or "what do I have?" question. |
|
|
25
|
+
| `get_journey` | Read full configuration for a specific journey (steps, exit rules, goal). |
|
|
26
|
+
| `list_journey_exits` | Read only the exit rules for one journey. Use when the user asks "what triggers an exit for this?" |
|
|
27
|
+
| `add_journey_exit` | Add an exit rule to a journey draft. Use when the user (or you proactively) decides a journey needs a new exit trigger. |
|
|
28
|
+
| `remove_journey_exit` | Remove an exit rule from a journey draft. |
|
|
29
|
+
| `audit_journey_exits` | Sweep across all published marketing journeys and find ones missing purchase exits. Run this proactively when a user mentions journeys, cart recovery, or marketing programs. |
|
|
30
|
+
| `get_contact_cart` | Inspect a contact's current cart state to debug cart-recovery journeys or branch.condition evaluations. |
|
|
31
|
+
|
|
32
|
+
## Core Concepts to Convey
|
|
33
|
+
|
|
34
|
+
### A journey is just steps with edges
|
|
35
|
+
|
|
36
|
+
Each step is one of: `send_email`, `send_push`, `send_sms`, `wait` (`fixed` or `until_event`), `branch.condition`, `goal`, or `exit`. Edges describe which step runs next. The runtime (a Step Functions state machine per execution) advances pointers and emits events.
|
|
37
|
+
|
|
38
|
+
### Exit Events are journey-level, not step-level
|
|
39
|
+
|
|
40
|
+
Exit rules are declared once at the journey level and apply to every in-flight execution. When a matching event fires for the executing subject, the matcher halts the execution **regardless of which step it was on**.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
journey.exitEvents = [
|
|
44
|
+
{
|
|
45
|
+
id: "exit-cart-purchased",
|
|
46
|
+
eventName: "in_app_purchase",
|
|
47
|
+
description: "Stop messaging if the user completes a purchase",
|
|
48
|
+
enabled: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "exit-cart-cleared",
|
|
52
|
+
eventName: "cart_snapshot",
|
|
53
|
+
guard: { kind: "compare", path: "itemCount", op: "==", value: 0 },
|
|
54
|
+
description: "Stop messaging if the cart was emptied (not via purchase)",
|
|
55
|
+
enabled: true,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
When teaching users, this is the mental model:
|
|
61
|
+
|
|
62
|
+
> "A journey starts because something happened. An exit fires when the reason for the journey no longer exists."
|
|
63
|
+
|
|
64
|
+
### Send-time gate is authoritative
|
|
65
|
+
|
|
66
|
+
Even if the stream-based exit matcher misses an event (race conditions are rare but possible), every `send_*` step re-checks exit rules immediately before firing. This is the guarantee Apex makes to its users: **if an exit event has fired by the moment we send, we will not send**.
|
|
67
|
+
|
|
68
|
+
You can mention this to users; it removes a class of "what if the event arrived 2ms before send?" worries.
|
|
69
|
+
|
|
70
|
+
### Counters that prove it works
|
|
71
|
+
|
|
72
|
+
`/api/journeys/[id]/calibrated-impact` exposes:
|
|
73
|
+
|
|
74
|
+
- `triggered_exposed_exited_before_send` — how many people exited before any message went out. **A healthy cart-recovery journey will have this > 0**: it's the number of customers Apex protected from a stale message.
|
|
75
|
+
- `converted_exposed_messaged` — converted users who received at least one message. This is the honest "did the message contribute?" denominator.
|
|
76
|
+
- `messagedLift` — calibrated lift restricted to messaged subjects.
|
|
77
|
+
|
|
78
|
+
When discussing journey performance, lead with `messagedLift`, not raw conversion.
|
|
79
|
+
|
|
80
|
+
## Tool Invocation Order
|
|
81
|
+
|
|
82
|
+
### "Set up cart abandonment recovery"
|
|
83
|
+
|
|
84
|
+
This is the canonical journey-skill workflow. **Always involve apex-communications** for the payloads.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
1. list_journeys → check if a cart-recovery journey already exists
|
|
88
|
+
2. If not, recommend the mobile-cart-recovery template (it's seeded in Apex)
|
|
89
|
+
3. Ensure communications exist: hand off to apex-communications.recommend_communications + generate_communications for the cart-recovery push and email
|
|
90
|
+
4. Confirm exit rules: add_journey_exit for in_app_purchase, cart_snapshot itemCount==0, and checkout_completed
|
|
91
|
+
5. list_journey_exits to confirm
|
|
92
|
+
6. Walk the user through the apex.trackCart helpers their app needs to call (see apex-growth-tracking for the SDK shape)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### "Audit my journeys for missing exits"
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
1. audit_journey_exits(active_only=true)
|
|
99
|
+
2. For each missing_purchase_exit row: walk the user through add_journey_exit
|
|
100
|
+
3. Re-run audit_journey_exits to confirm all journeys are green
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### "Why didn't this user get my cart-recovery push?"
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
1. get_contact_cart(contact_id=...) → check current cart state (itemCount, lastEventId)
|
|
107
|
+
2. If cart.itemCount === 0, the exit rule fired — that's correct behavior
|
|
108
|
+
3. If cart still has items, get_journey(journey_id) and verify the wait duration + exit rules
|
|
109
|
+
4. Check calibrated-impact for triggered_exposed_exited_before_send count
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### "Add a new exit condition to onboarding"
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
1. list_journeys → find the onboarding journey id
|
|
116
|
+
2. list_journey_exits(journey_id) → see what already exits
|
|
117
|
+
3. add_journey_exit({journey_id, event_name: "onboarding_completed", description: "User finished setup before wait expired"})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Proactive Behavior
|
|
121
|
+
|
|
122
|
+
### When the user creates a marketing or lifecycle communication
|
|
123
|
+
|
|
124
|
+
After helping a user generate a single communication, ask whether it's part of a sequence:
|
|
125
|
+
|
|
126
|
+
> "This 'cart abandoned' push is a single send. Most cart-recovery programs are a sequence: push at 30 min → email at 2 hr → final reminder at 24 hr. Want me to wire this as a journey so it doesn't fire for users who already bought?"
|
|
127
|
+
|
|
128
|
+
### When the user mentions a wait, delay, or "after X hours"
|
|
129
|
+
|
|
130
|
+
Anything with a wait needs exit rules. Don't ship a journey without them.
|
|
131
|
+
|
|
132
|
+
> "I set up the journey with a 1-hour wait before the next message. Before we publish, let me add exit rules so we stop messaging anyone whose cart is purchased or cleared during that hour."
|
|
133
|
+
|
|
134
|
+
### When auditing existing programs
|
|
135
|
+
|
|
136
|
+
Run `audit_journey_exits` proactively at the start of any conversation about journeys, marketing programs, or cart abandonment. If anything returns missing, surface it before doing anything else.
|
|
137
|
+
|
|
138
|
+
### When the user asks "should I send this?"
|
|
139
|
+
|
|
140
|
+
If the answer involves a wait, the real question is "what should stop this from sending?" Reframe the conversation toward exit rules.
|
|
141
|
+
|
|
142
|
+
## Common Mistakes to Catch
|
|
143
|
+
|
|
144
|
+
| Mistake | What to say |
|
|
145
|
+
|---|---|
|
|
146
|
+
| Cart-recovery journey with no exit on purchase | "Add an exit on `in_app_purchase`. Without it you'll message buyers after they bought." |
|
|
147
|
+
| `wait.fixed` for cart abandonment | "Use `wait.until_event` with a deadline. That way the wait ends as soon as the user comes back, not on a fixed timer." |
|
|
148
|
+
| Exit rule with no `guard` on `cart_snapshot` | "`cart_snapshot` fires whenever the cart changes, including adds. Add a guard like `itemCount == 0` so you only exit on a cleared cart." |
|
|
149
|
+
| Same exit rule on multiple journeys, hand-managed | "Apex doesn't share exit rules across journeys, but the underlying event matches the same. Each journey gets its own copy — that's fine." |
|
|
150
|
+
| Disabling all exit rules to "always send" | "If you want to always send, remove the wait. Don't disable exits and keep the wait — you'll re-create the original problem." |
|
|
151
|
+
|
|
152
|
+
## Glossary
|
|
153
|
+
|
|
154
|
+
- **AdaptiveJourney**: A draft/published journey with steps and exit rules.
|
|
155
|
+
- **ExitEventRule**: One rule on `journey.exitEvents`. Has `eventName`, optional `guard` predicate, `description`, `enabled`.
|
|
156
|
+
- **JourneyExecutionPointer**: Server-side record of an in-flight execution for one contact.
|
|
157
|
+
- **wait.until_event**: A wait step that ends when a named event fires (or a deadline expires).
|
|
158
|
+
- **branch.condition**: A step that routes execution based on a predicate over subject attributes (e.g., `cart.itemCount >= 2`).
|
|
159
|
+
- **Send-time gate**: The authoritative re-check that runs immediately before any send step.
|
|
160
|
+
|
|
161
|
+
## Resources
|
|
162
|
+
|
|
163
|
+
- `apex://journeys` — current journeys list with exit-rule counts (if available as an MCP resource)
|
|
164
|
+
|
|
165
|
+
## See Also
|
|
166
|
+
|
|
167
|
+
- **apex-communications** — for the per-message payloads (subject, body, channel) the journey fires
|
|
168
|
+
- **apex-growth-tracking** — for the `apex.trackCart` SDK helpers that emit the events journeys consume and exit on
|
|
169
|
+
- **apex-experimentation** — to A/B test variants within a journey step
|