@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.
@@ -1,53 +1,31 @@
1
1
  ---
2
2
  name: apex-integration-cookbook
3
- description: Recipes for integrating Apex into products — snippet installation, SDK wiring, form tracking, auth/signup events, Stripe lifecycle, communication triggers, connectors, and actuators. Use when the user is adding Apex to their product, connecting a data source, or wiring events to communications or experiments.
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 common Apex integration patterns. Each recipe shows what to instrument, which surface to use (snippet, SDK, or API), and what Apex does with the data.
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
- ## Surfaces overview
10
+ ## SDK setup
11
11
 
12
- | Surface | Best for | Install |
13
- |---------|----------|---------|
14
- | **Snippet** (`/api/apex-js`) | Marketing sites, landing pages, lightweight tracking | `<script src="https://your-apex/api/apex-js?key=PROJECT_KEY"></script>` |
15
- | **SDK** (`@apex-inc/sdk`) | Apps and backends — richer tracking, identity, management | `npm install @apex-inc/sdk` then `init({ projectKey })` |
16
- | **MCP** (`@apex-inc/mcp-server`) | AI-assisted setup — create experiments, communications, track events from the editor | One-click install from Apex dashboard |
17
- | **API** (direct HTTP) | Webhooks, server-side events, custom integrations | `POST /api/events` with `x-api-key` header |
18
-
19
- ## Recipe 1: Marketing site (snippet only)
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
- ## Recipe 2: App signup and authentication
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
- **Goal:** Track signup, login, and connect anonymous browsing to known users.
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 is critical — it links the anonymous marketing session to the authenticated user. Without it, Apex creates a disconnected identity.
42
+ The `visitorId` trait links the anonymous marketing session to the authenticated user. Without it, pre-signup attribution is lost.
65
43
 
66
- ## Recipe 3: Onboarding funnel
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 communications in the Apex catalog when tenant communications are active. The communication trigger system matches `triggerEventId` on active `TenantCommunication` records.
51
+ Events like `onboarding_completed` automatically match to active tenant communications when the trigger system is wired.
79
52
 
80
- ## Recipe 4: Stripe subscription lifecycle
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
- **Goal:** Track revenue events from Stripe webhooks.
60
+ // On cancellation
61
+ track("subscription_cancelled", { reason: "too_expensive" });
62
+ identify(userId, { lifecycleStage: "churned" });
63
+ ```
83
64
 
84
- Set up the Stripe connector via the dashboard (Settings → Connectors → Stripe), or handle webhooks directly:
65
+ ## Recipe 4: Feature usage
85
66
 
86
67
  ```typescript
87
- // In your Stripe webhook handler
88
- switch (event.type) {
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
- ## Recipe 5: Feature usage tracking
72
+ Use one event name with a `feature` property — not dozens of unique event names.
117
73
 
118
- **Goal:** Measure which features drive retention and activation.
74
+ ## Recipe 5: Form tracking (shortcut)
119
75
 
120
76
  ```typescript
121
- // Track meaningful feature interactions, not every click
122
- track("feature_used", { feature: "export_report", format: "csv" });
123
- track("feature_used", { feature: "invite_teammate" });
124
- track("feature_used", { feature: "create_experiment" });
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
- // Track milestone completions
127
- track("milestone_reached", { milestone: "first_experiment_completed" });
128
- track("milestone_reached", { milestone: "10_contacts_scored" });
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
- Prefer one event name (`feature_used`) with a `feature` property over dozens of unique event names. This keeps the event taxonomy clean and makes analysis easier.
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
- ## Recipe 6: Communication triggers
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
- **Goal:** Wire custom events to lifecycle communications.
132
+ ## Partner Network patterns
136
133
 
137
- Communications trigger automatically when:
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
- Common trigger events from the Apex event taxonomy:
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
- | Event | Communication |
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
- Use the MCP tool `get_event_taxonomy` or `GET /api/communications/events?vertical=b2b-saas` to see the full recommended event list for your vertical.
140
+ ### Run a manual-approval program + vet applicants in a script
154
141
 
155
- ## Recipe 7: Outbound webhooks (actuators)
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
- **Goal:** Push Apex events to external systems (Slack, CRMs, custom endpoints).
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
- Configure actuators in the dashboard (Settings → Actuators) or via API. Apex sends signed payloads:
161
+ ### Batch-approve the weekly payout queue
160
162
 
161
- ```json
162
- {
163
- "type": "experiment.completed",
164
- "payload": { "experimentId": "...", "winner": "variant_b", "confidence": 0.95 },
165
- "timestamp": "2026-04-15T..."
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
- Verify with `X-Apex-Signature` header (HMAC-SHA256 of the body with your webhook secret).
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
- Available trigger events: `experiment.completed`, `experiment.started`, `model.drift`, `sensor.sync_complete`, `sensor.sync_failed`, `budget.threshold`, and more.
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
- ## Recipe 8: Server-side event ingestion
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
- **Goal:** Send events from a backend without the browser SDK.
183
+ ### Disclose fees accurately in your UI
176
184
 
177
- ```bash
178
- curl -X POST https://your-apex-url/api/events \
179
- -H "Content-Type: application/json" \
180
- -H "x-api-key: YOUR_API_KEY" \
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
- ## Event naming conventions
191
+ ## Recipe Mobile build sources (TestFlight vs App Store)
194
192
 
195
- - **snake_case** or **dot-separated**: `signup_completed`, `feature_used`, `invoice.paid`
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
- ## Identity stitching checklist
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 a user transitions from anonymous to known:
231
+ ### When NOT to recommend a separate project
203
232
 
204
- 1. Call `identify(userId, { email, visitorId: apex_vid_cookie_value })`
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 naming and tracking fundamentals: see **apex-growth-tracking** skill
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