@hogsend/cli 0.2.0 → 0.2.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/package.json +2 -2
- package/skills/hogsend-authoring-journeys/SKILL.md +1 -1
- package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +24 -0
- package/skills/hogsend-authoring-journeys/references/journey-context.md +23 -0
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hogsend/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"tsup": "^8.5.1",
|
|
33
33
|
"tsx": "^4.22.4",
|
|
34
34
|
"vitest": "^4.1.7",
|
|
35
|
-
"@hogsend/studio": "^0.
|
|
35
|
+
"@hogsend/studio": "^0.4.0",
|
|
36
36
|
"@repo/typescript-config": "0.0.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
@@ -59,7 +59,7 @@ export const welcome = defineJourney({
|
|
|
59
59
|
## Key concepts
|
|
60
60
|
|
|
61
61
|
- **`ctx` is orchestration primitives ONLY** — `sleep`, `sleepUntil`, `when`,
|
|
62
|
-
`checkpoint`, `trigger`, `identify`, `guard.isSubscribed`,
|
|
62
|
+
`waitForEvent`, `checkpoint`, `trigger`, `identify`, `guard.isSubscribed`,
|
|
63
63
|
`history.hasEvent/journey/email`, `posthog.capture`. Features are standalone
|
|
64
64
|
imports: `sendEmail()` and `getPostHog()` come from `@hogsend/engine`, NOT off
|
|
65
65
|
`ctx`.
|
|
@@ -63,6 +63,30 @@ export const activation = defineJourney({
|
|
|
63
63
|
after a long `ctx.sleep` — a user can unsubscribe during the wait. Enrollment
|
|
64
64
|
only checks preferences at entry, not at each send.
|
|
65
65
|
|
|
66
|
+
## Reactive alternative: `ctx.waitForEvent`
|
|
67
|
+
|
|
68
|
+
The pattern above sleeps a **fixed window** then polls history — good when you
|
|
69
|
+
want to wait a set time regardless. When you're waiting **for a specific event to
|
|
70
|
+
happen** (and want to react the moment it does, or give up after a deadline),
|
|
71
|
+
`ctx.waitForEvent` is the sharper tool: it resumes the instant the user fires the
|
|
72
|
+
event, or returns `timedOut: true` when the timeout wins.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const { timedOut } = await ctx.waitForEvent({
|
|
76
|
+
event: Events.FEATURE_USED,
|
|
77
|
+
timeout: days(7), // required; capped at the 720h task limit
|
|
78
|
+
label: "await-activation",
|
|
79
|
+
});
|
|
80
|
+
if (!timedOut) return; // they activated on their own — done
|
|
81
|
+
if (!(await ctx.guard.isSubscribed())) return;
|
|
82
|
+
await sendEmail({ /* nudge */ });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Rule of thumb: **fixed delay → `ctx.sleep` then `ctx.history.hasEvent`**;
|
|
86
|
+
**wait until they do X (or time out) → `ctx.waitForEvent`**. The wait is
|
|
87
|
+
forward-looking (only events after it begins count), and an `exitOn` match
|
|
88
|
+
cancels it mid-wait, so no post-wait send fires after the journey exits.
|
|
89
|
+
|
|
66
90
|
## Idempotency — journeys can replay
|
|
67
91
|
|
|
68
92
|
A journey task is durable, and a step before a `ctx.sleep` can be re-executed if
|
|
@@ -29,6 +29,29 @@ await ctx.sleepUntil(at, { label: "morning-nudge" });
|
|
|
29
29
|
// .in(days(3)).at("HH:mm"), and chainers .tz(zone) / .window(start,end) / .ifPast("next"|"now")
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Durable wait-for-event
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// Park the journey until THIS user emits `event`, OR `timeout` elapses —
|
|
36
|
+
// whichever first. The reactive alternative to "sleep a fixed window, then poll
|
|
37
|
+
// ctx.history": it resumes the INSTANT the event lands. Forward-looking — only
|
|
38
|
+
// events fired AFTER the wait begins count (use ctx.history.hasEvent for the past).
|
|
39
|
+
const { timedOut } = await ctx.waitForEvent({
|
|
40
|
+
event: Events.FEATURE_USED,
|
|
41
|
+
timeout: days(7), // REQUIRED, capped at the 720h task execution limit
|
|
42
|
+
label: "await-activation", // optional — written as currentNodeId
|
|
43
|
+
});
|
|
44
|
+
if (timedOut) {
|
|
45
|
+
// they never did it — nudge (re-check ctx.guard.isSubscribed() first after a long wait)
|
|
46
|
+
} else {
|
|
47
|
+
// event arrived — they activated on their own
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If the journey `exitOn`-matches (or is cancelled) WHILE waiting, the run aborts
|
|
52
|
+
cleanly — state goes `"exited"`, the durable run is cancelled, and no post-wait
|
|
53
|
+
step (or email) fires. You don't catch anything; the engine handles it.
|
|
54
|
+
|
|
32
55
|
## Observability
|
|
33
56
|
|
|
34
57
|
```ts
|
|
@@ -129,13 +129,16 @@ overlapping runs of the same journey.
|
|
|
129
129
|
A `journeyStates` row tracks each run. Once the gates pass:
|
|
130
130
|
|
|
131
131
|
- **enter** → row created with `status: "active"`, `currentNodeId: "start"`.
|
|
132
|
-
- **`ctx.sleep` / `ctx.sleepUntil`** → `status: "waiting"`
|
|
133
|
-
to `"active"` on resume.
|
|
132
|
+
- **`ctx.sleep` / `ctx.sleepUntil` / `ctx.waitForEvent`** → `status: "waiting"`
|
|
133
|
+
while suspended, back to `"active"` on resume.
|
|
134
134
|
- **`run()` returns** → `status: "completed"`, `completedAt` set, and a
|
|
135
135
|
`journey:completed` event is pushed.
|
|
136
136
|
- **`run()` throws** → `status: "failed"`, `errorMessage` recorded, and a
|
|
137
137
|
`journey:failed` event is pushed; the error re-throws so Hatchet sees the
|
|
138
138
|
failure.
|
|
139
|
+
- **`exitOn` matches (or cancelled)** → `status: "exited"`. If it happens while
|
|
140
|
+
the journey is suspended in a `ctx.sleep`/`ctx.waitForEvent`, the durable run
|
|
141
|
+
is cancelled so no further step runs — even mid-wait.
|
|
139
142
|
|
|
140
143
|
Because the gates run before any state is created, a skipped event is invisible
|
|
141
144
|
in `journeyStates` — to debug "why didn't this user enroll?", check the gate
|